Your first show
In this tutorial you’ll write a complete show.json from scratch: a light strip
that toggles a rainbow animation with one button, plus a second button for a
“boop” flash. No code, no reflashing — just a JSON file.
Before you start
Section titled “Before you start”You need a working device. If you haven’t set one up yet, follow Quickstart: Standalone or Quickstart: Loader first, then come back. (No hardware? The desktop preview runs the same engine in your browser and works for this whole tutorial.)
You’ll be editing the show.json file on your device — either on its USB drive
or in the web dashboard’s Files tab. Each time you save, the dashboard shows
a pass/fail validation banner (full report under Console ▸ validation). On the
desktop preview, changes hot-reload live; a device re-reads the show on
reload/reboot.
Step 1 — the skeleton
Section titled “Step 1 — the skeleton”Start with the minimum valid show: a schema version and a state machine with one (empty) state.
{ "schemaVersion": 1, "name": "First Light", "board": "esp32-s3-devkitc-1", // use YOUR board's id — see its board page "states": { "initial": "Off", "Off": { } }}The board field lets the validator check your pins against real hardware.
Show files are JSONC, so // comments are fine.
What you should see: the validation banner passes. Nothing else happens yet — there’s no hardware declared.
Step 2 — declare the strip
Section titled “Step 2 — declare the strip”Add a devices section above states, and make the Off state prove the
wiring by painting the strip on entry:
"devices": { "strip": { "kind": "ledstrip", "count": 16, "pin": 45, "order": "GRB" }},"states": { "initial": "Off", "Off": { "enter": [ { "set": "@strip", "color": "#003311" } ] }}Match count, pin, and order to your actual strip — the pin is validated
against your board, so a wrong pin shows up as a warning, not a mystery.
Everywhere else in the show, the strip is just @strip; nothing but this one
line ever mentions the pin.
What you should see: on reload, the strip glows a dim green. Your hardware declaration works.
Step 3 — add a button
Section titled “Step 3 — add a button”Add an inputs section. We’ll make it virtual (no pin), so it appears as
a button on the web dashboard — perfect for testing:
"inputs": { "go": { "label": "GO", "color": "#39ff14" }}Have a real button wired up? Add pin details and it becomes physical:
{ "label": "GO", "color": "#39ff14", "pin": 7, "activeLow": true, "pull": "up" }
for a button to ground.
What you should see: a GO button appears on the dashboard — but it’s dark. Buttons only light up in states that handle their events, and no state does yet.
Step 4 — an idle look, and the toggle
Section titled “Step 4 — an idle look, and the toggle”Now the fun part. Add a looping script — a rainbow comet sweeping the strip —
and split the state machine into Off and On:
"scripts": { "idle": { "loop": true, "children": [ { "comet": "@strip", "color": "hsv(time*0.1, 1, 1)", "lapSec": 2.0, "tail": 6, "loop": true } ] }},"states": { "initial": "Off", "Off": { "enter": [ { "set": "@strip", "color": "#000000" } ], "on": { "go.tapped": "On" } }, "On": { "enter": [ { "start": "idle" } ], "exit": [ { "stop": "idle" } ], "on": { "go.tapped": "Off" } }}Read it as English: start in Off with the strip blanked. A tap of GO goes to
On, which starts the idle script on entry and stops it on exit. Another
tap goes back to Off. The button is momentary — the states remember which side
you’re on, which is why go.tapped appears in both.
The comet’s color is a live expression: hsv(time*0.1, 1, 1) cycles the hue
as the built-in time var counts seconds. Expressions work anywhere a color or
number does.
What you should see: GO is now lit in both states. Tap it — a rainbow comet chases around your strip. Tap again — dark.
Step 5 — the flourish: a boop button
Section titled “Step 5 — the flourish: a boop button”One more input, and a one-shot script that flashes the whole strip white over the top of the animation:
"inputs": { "go": { "label": "GO", "color": "#39ff14" }, "boop": { "label": "BOOP", "color": "#2ca7ff" }},"scripts": { "idle": { /* ...unchanged... */ }, "blink": { "children": [ { "play": "boop.wav", "flow": "detached" }, { "set": "@strip", "color": "#ffffff", "sec": 0.15 } ] }}Then give the On state a handler that runs it:
"On": { "enter": [ { "start": "idle" } ], "exit": [ { "stop": "idle" } ], "on": { "boop.tapped": { "do": [ { "run": "blink" } ] }, "go.tapped": "Off" }}Two details worth noticing:
- The
sethas"sec": 0.15— that holds the white for 0.15 s, re-asserting it every frame so it overrides the comet painting the same strip underneath. Withoutsec, your flash would last one frame and vanish. - The
playisflow: "detached"so the sound fires and forgets, overlapping the flash instead of delaying it.
What you should see: in On, the BOOP button is lit; tap it and the strip flashes white (with a boop, if you added the sound), then the rainbow resumes. In Off, BOOP is dark — that state has no handler for it.
The complete file
Section titled “The complete file”{ "schemaVersion": 1, "name": "First Light", "board": "esp32-s3-devkitc-1", // use YOUR board's id
"devices": { "strip": { "kind": "ledstrip", "count": 16, "pin": 45, "order": "GRB" } },
"inputs": { "go": { "label": "GO", "color": "#39ff14" }, "boop": { "label": "BOOP", "color": "#2ca7ff" } },
"scripts": { "idle": { "loop": true, "children": [ { "comet": "@strip", "color": "hsv(time*0.1, 1, 1)", "lapSec": 2.0, "tail": 6, "loop": true } ] }, "blink": { "children": [ { "play": "boop.wav", "flow": "detached" }, { "set": "@strip", "color": "#ffffff", "sec": 0.15 } ] } },
"states": { "initial": "Off", "Off": { "enter": [ { "set": "@strip", "color": "#000000" } ], "on": { "go.tapped": "On" } }, "On": { "enter": [ { "start": "idle" } ], "exit": [ { "stop": "idle" } ], "on": { "boop.tapped": { "do": [ { "run": "blink" } ] }, "go.tapped": "Off" } } }}Where next
Section titled “Where next”You’ve now used every core concept: devices, inputs, scripts, flow, and states. From here:
- Prefer hold-to-run over toggle? Handle
go.pressed/go.releasedinstead ofgo.tapped— the input itself never changes. - Add a
speedto theidlescript, point it at a var, andrampthe var from a state — the whole look (and any audio) glides faster or slower. - Every field, verb, and event in one place: Show format.
- Keep the one-pager beside you while you write: Cheatsheet.