A React renderer for Awtrix 3 LED matrix displays. Write JSX, see pixels.
import { App, Rect, Text, render } from "react-awtrix";
render(
<App icon="87" duration={15}>
<Rect x={0} y={0} width={32} height={8} color="#0D1117" filled />
<Text x={7} y={1} color="#58A6FF">
Hello
</Text>
</App>,
{ host: "192.168.1.45", app: "greeting" },
);State changes re-render and push to the device automatically. useState, useEffect, useRef and everything else from React 19 works as expected.
bun add react-awtrixPeer dependencies: react ^19.0.0 and typescript ^5.
Set the AWTRIX_HOST environment variable to your device IP, then:
import { useState, useEffect } from "react";
import { App, Rect, Text, render } from "react-awtrix";
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
const time = now.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
return (
<App icon="66" duration={10}>
<Rect x={0} y={0} width={32} height={8} color="#000814" filled />
<Text x={7} y={1} color="#E0E0E0">
{time}
</Text>
</App>
);
}
const handle = render(<Clock />, {
host: process.env.AWTRIX_HOST!,
app: "clock",
});
process.on("SIGINT", () => handle.unmount());All drawing happens on a 32x8 pixel matrix (configurable). Coordinates start at (0, 0) in the top-left corner.
Top-level container that sets app-level options. Every render tree should have one.
<App
icon="87"
duration={15}
background="#0D1117"
effect="Pulse"
effectSettings={{ speed: 3, palette: "Rainbow" }}
progress={75}
progressC="#2ECC71"
progressBC="#1A1F26"
pushIcon={1}
noScroll
center
>
{children}
</App>| Prop | Type | Description |
|---|---|---|
icon |
string |
Icon ID from the Awtrix icon database |
duration |
number |
Display duration in seconds |
lifetime |
number |
Lifetime in seconds before the app is removed |
lifetimeMode |
0 | 1 |
0 = remove after lifetime, 1 = mark stale |
text |
string |
Native Awtrix text (separate from draw commands) |
textCase |
0 | 1 | 2 |
0 = global, 1 = uppercase, 2 = as-is |
topText |
boolean |
Draw text at the top of the display |
textOffset |
number |
Text X offset |
center |
boolean |
Center text horizontally |
noScroll |
boolean |
Disable text scrolling |
scrollSpeed |
number |
Scroll speed percentage |
background |
Color |
Background color |
rainbow |
boolean |
Rainbow text effect |
effect |
string |
Background effect name |
effectSettings |
object |
Effect config: { speed?, palette?, blend? } |
overlay |
string |
Weather overlay: "clear", "snow", "rain", "drizzle", "storm", "thunder", or "frost" |
progress |
number |
Progress bar value (0--100) |
progressC |
Color |
Progress bar color |
progressBC |
Color |
Progress bar background color |
bar |
number[] |
Bar chart data |
line |
number[] |
Line chart data |
pushIcon |
0 | 1 | 2 |
0 = static, 1 = push once, 2 = push with animation |
repeat |
number |
Repeat count |
save |
boolean |
Save to flash |
All drawing components accept colors as hex strings ("#FF0000") or RGB tuples ([255, 0, 0]).
<Text x={0} y={1} color="#FFFFFF" maxWidth={24}>
Score: {score}
</Text>| Prop | Type | Description |
|---|---|---|
x |
number |
X position |
y |
number |
Y position |
color |
Color |
Text color |
maxWidth |
number |
Maximum width in pixels (clips text) |
charWidth |
number |
Character width for clipping calculation (default: 4) |
children |
string | number |
Text content |
<Rect x={0} y={0} width={32} height={8} color="#1A1A2E" filled />| Prop | Type | Description |
|---|---|---|
x, y |
number |
Top-left corner |
width, height |
number |
Dimensions |
color |
Color |
Stroke/fill color |
filled |
boolean |
Fill the rectangle |
<Pixel x={15} y={3} color="#FF6B6B" />| Prop | Type | Description |
|---|---|---|
x, y |
number |
Position |
color |
Color |
Pixel color |
<Line x1={0} y1={0} x2={31} y2={7} color="#4ECDC4" />| Prop | Type | Description |
|---|---|---|
x1, y1 |
number |
Start point |
x2, y2 |
number |
End point |
color |
Color |
Line color |
<Circle x={16} y={4} radius={3} color="#FFE66D" filled />| Prop | Type | Description |
|---|---|---|
x, y |
number |
Center |
radius |
number |
Radius |
color |
Color |
Stroke/fill color |
filled |
boolean |
Fill the circle |
<Bitmap x={0} y={0} width={2} height={2} data={[0xff0000, 0x00ff00, 0x0000ff, 0xffff00]} />| Prop | Type | Description |
|---|---|---|
x, y |
number |
Top-left corner |
width, height |
number |
Dimensions |
data |
number[] |
Pixel data (row-major, 24-bit RGB) |
Renders a React tree as a named custom app. Re-renders push updates to the device automatically.
import { render } from "react-awtrix";
const handle = render(<MyApp />, {
host: "192.168.1.45",
app: "my_app",
debounce: 50, // ms between device updates (default: 50)
debug: false, // log serialized payloads
width: 32, // matrix width (default: 32)
height: 8, // matrix height (default: 8)
});
// Later:
await handle.unmount(); // detach tree + delete app from deviceRenders a React tree as a notification. Resolves when sent.
import { notify } from "react-awtrix";
await notify(
<App icon="alert">
<Text x={7} y={1} color="#FF0000">
ALERT
</Text>
</App>,
{
host: "192.168.1.45",
hold: true, // keep notification until dismissed
sound: "alarm", // play a sound
stack: true, // stack with other notifications
wakeup: true, // wake the display
},
);For applications that manage multiple apps on one device, createRuntime() provides a shared transport with automatic cleanup.
import { App, Text, createRuntime } from "react-awtrix";
const runtime = createRuntime({
host: process.env.AWTRIX_HOST!,
debounce: 50,
});
runtime.app(
"clock",
<App icon="66">
<Text x={7} y={1} color="#E0E0E0">
12:00
</Text>
</App>,
);
runtime.app(
"status",
<App>
<Text x={1} y={1} color="#2ECC71">
OK
</Text>
</App>,
);
// Update an app by calling runtime.app() again with the same name
// Remove an app:
await runtime.remove("status");
// Clean shutdown (unmounts all apps, deletes from device):
await runtime.dispose();
// Or handle SIGINT/SIGTERM automatically:
runtime.handleSignals();The runtime supports bun --hot for live reloading during development. Apps that are no longer registered after a module re-evaluation are automatically removed from the device.
const runtime = createRuntime({
host: process.env.AWTRIX_HOST!,
hmr: true,
});
runtime.app("clock", <ClockApp />);
runtime.app("weather", <WeatherApp />);
runtime.handleSignals();bun --hot app.tsxEdit the file, save, and the display updates instantly. Remove an app() call, and that app disappears from the device.
The transport layer is pluggable. host/port options default to HTTP, or you can pass a protocol explicitly.
import { http, render } from "react-awtrix";
render(<MyApp />, {
app: "demo",
protocol: http({ host: "192.168.1.45", port: 80 }),
});Direct HTTP to the Awtrix API. Simple, no broker needed. Does not support event subscriptions.
import { mqtt, createRuntime } from "react-awtrix";
const runtime = createRuntime({
protocol: mqtt({
broker: "mqtt://broker.hivemq.com:1883",
prefix: "awtrix_abcdef",
}),
});Communicates via MQTT topics. Supports event subscriptions (button presses, app changes, device stats). Connections are pooled and reference-counted when multiple protocol instances share a broker.
For local development, you can run an in-process MQTT broker -- see examples/mqtt.tsx.
Event subscriptions are available on runtimes using the MQTT protocol:
runtime.on("button:left", ({ pressed }) => {
console.log("Left button", pressed ? "pressed" : "released");
});
runtime.on("button:select", ({ pressed }) => {
/* ... */
});
runtime.on("button:right", ({ pressed }) => {
/* ... */
});
runtime.on("currentApp", ({ name }) => {
/* ... */
});
runtime.on("stats", ({ value }) => {
/* ... */
});
runtime.on("device", ({ online }) => {
/* ... */
});HTTP does not support events. Calling runtime.on() with an HTTP protocol throws.
All examples read AWTRIX_HOST from the environment:
export AWTRIX_HOST=192.168.1.45| Example | Command | Description |
|---|---|---|
| Hello | bun run example:hello |
Static text with background |
| Clock | bun run example:clock |
Live clock with useState / useEffect |
| Progress | bun run example:progress |
Animated progress bar |
| Complex | bun run example:complex |
All drawing primitives, animated charts, bitmap spinner |
| Pomodoro | bun run example:pomodoro |
Pomodoro timer with phase management |
| MQTT | bun run example:mqtt |
MQTT transport with optional local broker |
| Server | bun run example:server |
Multi-app runtime with HMR (bun --hot) |
bun install # install dependencies
bun test # run tests (58 tests)
bun run typecheck # type check
bun run lint # lint with oxlint
bun run format # format with oxfmt
bun run knip # detect unused code
bun run check # all of the above
bun run build # build with tsdownJSX Components
|
v
react-reconciler (Concurrent Mode, React 19)
|
v
Instance Tree (AwtrixInstance nodes)
|
v
Serializer (tree -> Awtrix API payload + draw commands)
|
v
DeviceTransport (queue, coalescing, rate limiting)
|
v
Protocol (HTTP or MQTT) -> Awtrix 3 device
The reconciler maintains a virtual tree of display elements. On every React commit, the tree is serialized into an Awtrix custom app payload containing draw commands (dp, dl, dr, df, dc, dt, db). The transport layer coalesces rapid updates (latest-write-wins per app name) and pushes them to the device over the configured protocol.
MIT