Theme
The primitives draw from a set of CSS variables, the theme tokens, in @spawnite/ui’s stylesheet. A game restyles them by setting the variables on :root in its own stylesheet, and every primitive that reads a token follows: the panes, the menus, the loading screen, a health bar, an accent Button. A game sets a token once, not a colour on each element. A game that wants a look without choosing tokens imports one of the shipped themes, and one that wants more than a token can give restyles a primitive’s parts.
Unity’s UI Toolkit and Godot both theme controls through named variables that every control reads, rather than a game restyling each control’s inner parts. The engine matches them, and lists the variables here, because an agent reads this page where a person opens an inspector.
Set a token in the game’s src/styles.css, after the import of ui’s stylesheet:
@import "@spawnite/ui/styles.css";
:root { --color-menu-accent: var(--color-amber-300); --color-menu-accent-ink: var(--color-neutral-950); --font-display: "Bricolage Grotesque", var(--font-sans);}Not in @theme: a game’s stylesheet is not a Tailwind root, so an @theme block there reaches the browser unread. Each token is also a Tailwind colour, so bg-menu-accent and text-danger are classes a game’s own elements can wear, and they follow the override too.
The build warns when a game’s stylesheet holds an @theme block. A game adds its own animation as a plain class in @layer components with a top-level @keyframes, as Holdfast’s stylesheet does.
Themes
Section titled “Themes”Two themes ship in @spawnite/ui/themes/. Each is a stylesheet that sets every token on :root, so one import after ui’s stylesheet restyles every primitive, and a token set after the import goes past the theme:
| Theme | Look | Import |
|---|---|---|
| Glass | The default: translucent white panes over the world, white ink, an emerald bar. | none, or @import "@spawnite/ui/themes/glass.css"; |
| Slate | Opaque dark slabs with a hairline edge, pale ink, a green accent, no blur: Depthfield’s. | @import "@spawnite/ui/themes/slate.css"; |
spawnite create --theme slate writes the import into a new game’s src/styles.css, and the MCP’s new_game takes the same theme. In Storybook, the Theme toolbar switches every story between the two. Godot ships one default theme and a game loads its own .tres; Unity’s UI Toolkit ships a default and a runtime theme file per project. The engine ships two, the glass over the world and the slab, because they differ in kind where a third would differ in colour, and a game’s colours are its own tokens. Holdfast imports glass and sets every token after it in its src/ember.css, which spawnite/examples holds, the example of a theme carried into a game’s own look.
The tokens
Section titled “The tokens”Each token, what reads it, and its default:
| Token | Read by | Default |
|---|---|---|
--color-pane-surface |
The glass of a Panel, a glass Button, a Tap and a Modal’s pane | rgb(255 255 255 / 12%) |
--color-pane-sheen |
The top of the glass’s sheen, a gradient down the pane | rgb(255 255 255 / 25%) |
--color-pane-sheen-end |
The bottom of the sheen | rgb(255 255 255 / 5%) |
--color-pane-edge |
The glass’s edge, its inset highlight and its border | rgb(255 255 255 / 90%) |
--color-pane-ink |
Text on the glass, and the focus ring of a glass control | white |
--color-pane-shadow |
The shadow under a pane | rgb(0 0 0 / 25%) |
--radius-pane |
The corner of a Panel and a Modal’s pane, the rounded-pane class |
1.25rem |
--radius-control |
The corner of a Button, the rounded-control class |
1.25rem |
--blur-pane |
How far the world blurs behind the glass, the backdrop-blur-pane class; 0px for a slab |
16px |
--color-menu-surface |
The pane of the Escape menu, the performance overlay, a pane Button and menuPane |
rgb(12 14 20 / 88%) |
--color-menu-raised |
A control at rest in a menu | rgb(255 255 255 / 7%) |
--color-menu-edge |
The hairline edge of a menu pane and a pane Button | rgb(255 255 255 / 14%) |
--color-menu-muted |
Secondary text in a menu, the muted microphone | rgb(255 255 255 / 58%) |
--color-menu-accent |
What is on, chosen or focused in a menu; the fill of an accent Button; a menu slider’s track | white |
--color-menu-accent-ink |
Text on the accent | neutral-950 |
--color-menu-ink |
The text a NumberField, a TextField or a Select holds | white |
--radius-field |
The corner of a NumberField, a TextField or a Select | 0.375rem |
--text-field |
The size of the text a NumberField, a TextField or a Select holds | 15px |
--color-health-fill |
A Bar’s fill, unless the Bar names a color |
emerald-400 |
--color-health-track |
A Bar’s track | emerald-950 |
--color-health-loss |
The piece a blow just took, flashed before it drains | white |
--color-danger |
A readout past a limit, palette.dangerText |
red-400 |
--color-loading-background |
The loading screen’s field, a LoadingRing’s, the Made with notice’s | neutral-950 |
--color-loading-label |
The loading screen’s muted words | neutral-400 |
--color-loading-indicator |
The loading screen’s marks | neutral-200 |
--color-loading-bar |
The loading bar’s fill | neutral-200 |
--color-loading-track |
The loading bar’s track | rgb(255 255 255 / 14%) |
--color-loading-text |
The loading screen’s text | neutral-100 |
--color-stick-ring |
The Joystick’s ring | rgb(255 255 255 / 70%) |
--color-stick-knob |
The Joystick’s knob | rgb(255 255 255 / 90%) |
--color-reading-good |
A performance reading within budget | emerald-400 |
--color-reading-warn |
A reading near the budget | amber-400 |
--color-reading-bad |
A reading past it | red-400 |
--color-voice-speaking |
The microphone’s level while the room hears the player | emerald-400 |
--color-voice-muted |
The slash across a muted microphone | red-500 |
--font-display |
Titles and big numbers: a Heading, a LevelBanner, the font-display class |
var(--font-sans) |
A Tailwind name in the Default column, such as emerald-400, is that colour from Tailwind’s palette, var(--color-emerald-400).
A game’s own elements take a token through the class that names it, border-pane-edge or text-menu-accent, rather than a palette class such as text-amber-200, so one change in styles.css restyles the HUD and the menus together.
A token restyles every primitive at once. For one primitive that should look different from the rest, each primitive with more than one visible part takes classNames, a map from a part’s name to the classes it wears, beside the className that goes on its root. Each part’s classes merge over the primitive’s own, so classNames={{ value: "text-amber-300" }} recolours a Counter’s number and leaves its icon. Mantine and CharacterUI name their parts the same way, and an agent that has styled either knows the shape.
| Primitive | Parts |
|---|---|
| Button | root, icon |
| Counter | root, icon, value |
| Bar | root, track, fill, reserved, value |
| Modal | dialog, pane, title, overline, actions |
import { Counter } from "@spawnite/engine";
export function Streak({ streak }: { streak: number }) { return ( <Counter icon="flame" label="Streak" value={streak} classNames={{ icon: "text-menu-accent", value: "font-display" }} /> );}A Panel is one surface, so it takes className alone, and a Joystick and a Tap take the engine’s tokens and a className on the control.