Skip to content

Widget elements

A widget spec’s elements array is a flat list of typed objects drawn in order — later elements sit on top of earlier ones. This page is the field reference for every element type, the fields every element shares, and the higher-level structures (pages, layers, menu theming) that sit around them.

For the file layout, top-level spec fields, and the serial protocol, see Widget spec. For live data binding and events, see Data & events.

Every element type accepts these fields, in addition to whatever fields are specific to its type:

field meaning
t element type (label, bar, arc, img, panel, led, line, qr, btn, slider, toggle)
x y position, top-left origin
c main color, a hex string like "1DB954"
bind data key to subscribe to — the element refreshes live when the key updates
fmt text template, e.g. "{v}%"{v} is replaced with the bound value. Never a printf format string
show layer visibility key — see Layers below
cbind color bind — the key’s hex value drives the element’s accent color live (bars that go red past 90%, panels that flash on alerts)
obind opacity bind — the key’s value (0-255) fades the element live
o static opacity, 0-255, on any element
pulse 1 = infinite opacity breathe — blinking alert LEDs with zero PC traffic
{ "t": "label", "x": 10, "y": 6, "w": 120, "s": 14, "c": "8E8E93", "text": "CPU" }

"sym": "wifi" turns a label or btn’s text into an LVGL symbol glyph instead of text. Friendly names include play pause wifi home settings warning bell power trash refresh up down left right ok close volume_max mute battery_full charge bluetooth usb gps eye_open and more, or you can pass raw LV_SYMBOL_* names. Symbol font sizes run 12-48.

Combine sym with bind to make the data value itself the symbol name — useful for a live status icon:

WDATA,conn,wifi → shows the wifi glyph
WDATA,conn,close → flips to the close glyph

While a custom widget is active, the PC auto-pushes _time (12h), _time24, _ampm, and _date every 15 seconds — no element setup needed beyond binding to them. A watchface is just:

{ "t": "label", "s": 38, "bind": "_time" }
field meaning
w width in px — text truncates with ; omit for auto width
s font size: 12 / 14 / 16 / 18 / 22 / 38 (nearest bundled Montserrat)
a align within w: "c" center, "r" right (default left)
text static text, used when there’s no bind
bind + fmt live text, e.g. "fmt": "{v}°C"
scroll (v1.2) 1 = circular ticker for long text; needs w
{ "t": "label", "x": 10, "y": 22, "w": 120, "s": 38, "c": "FFFFFF",
"bind": "cpu_load", "fmt": "{v}%" }
field meaning
w h size (default 100×8); make h greater than w for a vertical bar (fills bottom-up)
c / bgc fill / track colors
r corner radius (default h/2)
bind min max value binding; the value is parsed as an integer and clamped to [min, max]
{ "t": "bar", "x": 10, "y": 72, "w": 130, "h": 8, "c": "1DB954", "bgc": "2C2C2E",
"bind": "cpu_load", "min": 0, "max": 100 }
field meaning
d diameter (default 64)
w ring thickness (default 6)
c / bgc indicator / track colors
bind min max value binding
fmt ls lc optional built-in centered value label — auto-centered in the ring, font size ls, color lc. Uses one extra element slot. Prefer this over a manually positioned label, which can’t truly center
a0 a1 rot (v1.2) background sweep in degrees, plus rotation — for speedometer-style partial gauges, e.g. "a0":0,"a1":270,"rot":135
{ "t": "arc", "x": 156, "y": 12, "d": 86, "w": 8, "c": "FF9F0A", "bgc": "2C2C2E",
"bind": "mem_load", "min": 0, "max": 100, "fmt": "{v}%", "ls": 18 }
field meaning
src filename inside the widget’s own folder, e.g. "logo.png" (RGB565-friendly PNG)
bind (v1.2) the data value picks which .png from the widget folder shows — weather icons, mode badges
{ "t": "img", "x": 20, "y": 20, "src": "logo.png" }
field meaning
w h r size + corner radius
c o fill color + opacity, 0-255
field meaning
d diameter (default 14)
c / offc ON / OFF colors
bind a hex value sets that exact color; a truthy/falsy value maps to on/off
field meaning
w h size (default 100×2)
c o r color, opacity, radius
field meaning
d size (default 90 — mind the 140px panel height)
c / bgc dark / light module colors
text or bind content; a bound key re-renders the code live

btn, slider, and toggle are the only elements that can fire events back to the PC. Each requires a unique id, and each can carry a macro field so a button/toggle press can run a saved PC macro directly — zero-code path, no plugin needed. See Data & events for the full event/protocol picture and Macros for authoring macros.

field meaning
id required — event id sent to the PC
w h r size + radius (default 80×30, r 8)
c tc s background color, text color, font size
text or bind+fmt label — bindable, so the PC can relabel it live
hold 1 = a 600 ms long-press fires a hold event instead of press
macro PC macro name to run on press
page widget page to jump to on press (local, instant)

Tap → pressed visual → WEVENT,<widget>,<id>,press on release.

{ "t": "btn", "id": "ping", "x": 20, "y": 100, "w": 90, "h": 30,
"c": "2C2C2E", "tc": "FFFFFF", "text": "Ping" }
field meaning
id required — event id
w h size (default 140×12, horizontal)
c bgc kc fill / track / knob colors
min max val range + initial value
bind PC-authoritative value between drags (reconcile pattern)
macro PC macro run on release

Dragging sends a live WEVENT,<widget>,<id>,value=<n> (throttled ~150 ms) plus a final one on release. A drag owns the touch gesture — it never triggers a widget-swipe.

field meaning
id required — event id
w h size (default 46×26)
c bgc checked color / track color
bind state key — the PC pushes 1/0 and wins over the local flip
macro PC macro name run on every flip

Tap flips instantly (optimistic) and sends WEVENT,<widget>,<id>,on or off. If the toggle is bound, the plugin should echo the authoritative state back with push_widget_data — the same reconcile pattern used by slider.

"show": "<key>" makes an element visible only while that data key is truthy (truthy = non-empty and not 0 / off / false). Flip one WDATA key to swap whole element groups in or out — state layers, alert overlays, fake tabs — without touching pages.

{ "t": "label", "x": 10, "y": 40, "c": "FF3B30", "text": "LOW BATTERY",
"show": "low_batt" }

A widget can define up to 6 pages; the 48-element budget is shared across all of them.

{ "mixw": 1, "name": "X", "bg": "101018", "pages": [
{ "name": "Dash", "elements": [ /* ... */ ] },
{ "name": "Controls", "elements": [ /* ... */ ] },
{ "name": "My Menu", "hidden": true, "elements": [ /* ... */ ] }
] }
  1. Single-page specs need no pages key at all — just a top-level elements array.
  2. Tapping empty widget space opens an overlay menu of page names (like the clock’s sub-mode menu); tapping a row switches pages.
  3. btn elements can jump pages directly via their page field, bypassing the overlay.
  4. Mark a page "hidden": true to keep it out of the built-in tap menu while it stays reachable via btn page jumps or WPAGE — handy for hand-built menu pages and sub-screens.
  5. Every switch announces WEVENT,<widget>,_page,<name> so plugins can push page-specific data, and the PC can force a page with WPAGE,<name>.

The built-in tap-menu overlay can be restyled from a top-level "menu" block. All fields are optional — anything omitted keeps the stock look.

"menu": { "w": 180, "h": 32, "gap": 6, "r": 16, "s": 16,
"c": "26263A", "selc": "9A9AFF", "tc": "FFFFFF",
"bgc": "000000", "o": 190 }
field meaning field meaning
w h row size (clamped 60-236 / 18-60) c row color
gap space between rows selc current-selection color
r row corner radius tc row text color
s row font size bgc / o veil color / opacity, 0-255
  • Widget spec — file layout, top-level fields, and the serial protocol
  • Data & eventspush_widget_data, on_widget_event, pairing and namespacing
  • Widget builder — build specs visually instead of hand-writing JSON