Themes
A theme is one YAML file in a pack: literal colours assigned to a closed set of interface and syntax tokens. Themes restyle and never behave. There is no expression language, no selector, and no stylesheet — a value is #rrggbb or #rrggbbaa and nothing else, which is the whole of what a theme can say.
Start from a working palette
pw extensions new-theme --from charcoal --id acme/kit:nightshade --name Nightshade > \
my-pack/contributions/themes/nightshade.yaml
The file compiles before you change a thing. It lists the decisions someone actually made and leaves out anything the host can derive, so what you see is what is worth editing. Pass --full to print the complete vocabulary instead, and --from daylight to fork the light palette.
Six colours is a complete theme
id: acme/kit:nightshade
name: Nightshade
appearance: dark
tokens:
background: "#14121a"
text: "#e2ddf0"
accent: "#9a6fd0"
danger: "#ff6b81"
warning: "#e8b04b"
status-positive: "#6fd0a0"
syntax:
keyword: "#c792ea"
string: "#c3e88d"
comment: { color: "#8b93b8", italic: true }
Those six tokens are the decisions nothing can make for you. Every other token has a host derivation, and every syntax scope you leave unset folds to its parent — keyword.control follows keyword, an unset root follows body text. Set as few or as many as you like; both compile to a complete theme.
This is also why a theme keeps working. When a token is added to the vocabulary it ships with a derivation, so a theme written before it existed still compiles and still looks right.
What the host derives, and what it will not
Tints, diff washes, scrollbar mixes, shadows, and hover states are expressions over the tokens you set, so they follow your palette without appearing in your file. You cannot set them, rename them, or add to them.
Two pairs are worth knowing about:
- Diff colours are separate from status colours.
diff-add-hueanddiff-delete-huedefault to your success and danger colours, and you can override just those two to make every diff surface colourblind-safe without changing what success and failure mean elsewhere. - Status inks are derived from status hues.
warningis a fill, a wash, and a dot;warning-textis caution copy on a surface. The second is derived from the first and darkened or lightened only as far as readability requires, so you declare one colour and both jobs work.
Readability is checked, not assumed
A small closed list of pairs — body text on the page, status copy on a surface, labels on accent fills, ink over a selection band, both diff hues — is measured when your theme compiles. Fall under a minimum and the build fails with the pair, the ratio, and the reason. pw extensions validate prints the measured ratio for every gated pair, so you can see your margin rather than only that you passed.
Beyond colour
A theme may also set its handwriting and quiet the product mark:
icon_stroke— weight, cap, and join for every stroked glyph, including ones you never redrew. Weight multiplies each slot’s width, so relative emphasis survives.icons— replace the glyph inside a control from a closed shape vocabulary. The frame stays the host’s, so a themed glyph cannot resize a control or carry its own colour.brand.logomark: hidden— quiet the product mark. A theme can never substitute one, and hiding reserves its box so nothing moves. The square stays the product black under every palette, including light.
Reference
The complete vocabulary — every token, its fill rule, every syntax scope and what it folds to, every icon slot, and the contrast floor — is in theme-tokens.md. A JSON Schema ships alongside the app, so an editor pointed at theme-unit.schema.json completes token ids as you type.
Themes install like any other pack content: see Extension packs. Users pick one in Settings → General → Display; see Configuration.
Enable the pack for the device and its themes are on offer everywhere, including before a project is open. A pack enabled only for one project offers its themes while that project is open; picking one and moving on falls back to a shipped theme, named, until you return.