Skip to main content
6 min read

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

  1. c.sprite() string art with a legend — costs no asset, recolors per state (one glyph, many legends; scale=2 for a hero). See c.sprite.
  2. 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.
  3. 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:

NO LOCATION in amber over SET A ZIP in dim slate, on a near-black card
2. Error — a what and a what to do.
ALL CLEAR in green with a green rail and 0 ALERTS beneath
3. Empty — positive, never the amber card.
A large 72 F with an amber DEMO chip and ADD API KEY beneath
4. Demo — believable sample data, labelled.
  1. Live — the happy path.
  2. No data / error — the shared card. The classic form (nodata(c, title, sub), 41 apps byte-identical): background #0B0C12, centered amber #E8B04A title on the NODATA_FONTS ladder, dim slate #6A7090 sub, 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).
  3. 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.
  4. Demo — when a required key is absent. Believable sample data, labelled DEMO on the panel, promised in the input's help text.

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.

Check them the same way you check the happy path

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.

Next