Pixel art & the four screens
Prioritize graphics over text
Pixel art is the panel's native language. When a picture and a word compete for the same pixels, the picture wins and the text supports it. Never delete a working graphic to make room for a label — find the layout that keeps both, or shrink the label.
No magic numbers
Every number gets pixel art or a data label saying what it is. A bike-share app with 3–4 large numbers and no labels leaves the viewer guessing — available docks? free bikes? Temperatures always carry degrees. Units of measurement always carry a label. A label can be a pixel-art asset instead of text, but it must be intuitive to a 5th grader what the number represents on the finished app.
Pixel art quality (requirements)
- Pixel art must never interfere with other assets on the screen — including when it changes size. A growing or shrinking sprite must not overlap anything at any of its sizes.
- Pixel art should replace repetitive and obvious text while adding context. The best apps let the picture be the label — "the word 'BIKES' is not on the panel; the bike is."
- Pixel art must be pixel-perfect: no anti-aliasing, no semi-transparent pixels. Draw at native resolution and scale with nearest-neighbor interpolation only (the renderer's default), so pixels stay crisp. A fully transparent background behind hard-edged pixels is fine — it's soft alpha edges and interpolation blur that are banned. See Working with images.
Mechanisms, in order of preference
c.sprite()string art with a legend — costs no asset, recolors per state (one glyph, many legends;scale=2for a hero). Seec.sprite.- PNG assets — declared under
assets:, ≤ 128 KiB, drawn at the size they were authored. Ship the same filename as a real 24x24 and a real 16x16 (sz = 24 if c.width >= 128 else 16— the most repeated line in the catalog) rather than scaling one file. c.bitmap()0/1 matrices for tiny inline glyphs.
Every app has four screens, not one
The publish-time validator renders every page with the network disabled — a panel on a wall must say something sensible rather than going blank. Design all four:



- Live — the happy path.
- No data / error — the shared card. The classic form (
nodata(c, title, sub), 41 apps byte-identical): background#0B0C12, centered amber#E8B04Atitle on theNODATA_FONTSladder, dim slate#6A7090sub, on bands that can never overlap. The SCROLL form:rail(c, OFFLINE)+message(c, head, sub). Copy is two short uppercase lines — a what and a what to do:("NO LOCATION", "SET A ZIP"). Name the states separately (bad key ≠ rate-limited ≠ offline ≠ empty), and be actionable:"SHORTEN NAME AND PASSWORD BY " + str(over). - Empty — which is NOT an error. Zero storms, zero tasks, zero trains is the
answer people want: a green rail and a positive line (
"ALL CLEAR"), never the amber card, never a blank panel. - Demo — when a required key is absent. Believable sample data, labelled
DEMOon the panel, promised in the input'shelptext.
Degrade, don't die. A failed timezone lookup costs the offset, not the panel. Wrap every feed read in safe accessors — a raised host error kills the whole render.
In Studio, blank an input or type a nonsense zip and watch the panel. Over MCP,
render_app takes simulate_offline: true, and validate_app warns if an app
crashes with no network instead of falling back — see
Connect your AI.