This documents framework v1, which is deprecated. Games already on it keep running and their authors can keep editing them, but new games are created on the current framework — start with React frontends.
A theme.json is the game's structured theme document. It lives at /theme.json (game root, not under /src) and is stored as JSON — it is not imported as a code module. Themes are validated against a strict schema when you save them; an invalid theme is rejected with the offending field reported.
theme.json and /src/theme.html are two distinct artifacts. theme.json is the structured, allow-listed token document described here. /src/theme.html is a separate free-form HTML styling reference comp (see design-theme-instructions). They are not the same file and not interchangeable.
The top-level object is strict — unknown keys are rejected.
| Field | Type | Notes |
|---|---|---|
version |
1 |
Required literal. |
mood |
string? |
Optional, ≤64 chars. |
voice |
string? |
Optional, ≤64 chars. |
fonts |
{ heading, body, mono } |
Strict object, exactly these three keys. Each value is a font key — an identifier-shaped string (letters, digits, underscore). NOT a CSS font-family list, NOT a raw CSS string. |
colors |
Record<palette, Record<shade, color>> |
Map of palette name → (map of shade key → literal color). E.g. colors.accent["500"] = "#ef9f27". Shade keys are numeric ("50".."900") or named. Values are literal colors only — no references. |
semanticTokens |
Record<group, Record<token, { light, dark }>> |
Two-level map: group → token → { light, dark }. The pair object is strict (exactly light + dark). Each side is either a reference (see below) or a literal color. |
radii |
Record<shade, length> |
Length values (e.g. "4px"). |
shadows |
Record<name, "none" | ShadowLayer[]> |
Either the literal "none" or an array of structured shadow layers { offsetX, offsetY, blur, color, spread?, inset? }. Not a raw CSS shadow string. |
palettes |
Record<name, Record<key, color>> |
User-defined palettes (may be {}). Literal colors only — referenced from semantic tokens via the palettes.<name>.<key> form. |
spacing |
Record<shade, length> |
Length values. |
background |
{ light, dark }? |
Optional. Global board background. See its own subsection. |
defaultColorMode |
"light" | "dark"? |
Optional. Which side of semanticTokens / background paints on first load. When set, the theme is deterministic — every visitor sees this mode until they toggle. When unset, the page picks up the visitor's prefers-color-scheme (light OS → light, dark OS → dark); a ThemeToggle button still flips it explicitly. The live board follows the app's global theme regardless of this field. |
A value in semanticTokens (or a ref-or-color slot inside background) is either a literal color or one of exactly two reference forms:
"<palette>.<shade>" — into top-level colors (e.g. "accent.500" → colors.accent["500"])."palettes.<name>.<key>" — into the palettes block (e.g. "palettes.warmth.bg" → palettes.warmth.bg).Any other string shape is rejected. References resolve through chains up to depth 8; cycles and dangling references are rejected. colors and palettes are the leaves of the reference graph — a reference-shaped string inside them is rejected.
background (optional)When present, background is { light, dark }, each a background variant:
baseColor — a ref-or-color. The base fill painted behind all layers.layers — an ordered array of up to 8 layer objects, painted bottom-to-top. Each layer is a closed shape selected by its kind:"solid" — { kind, color }. color is a ref-or-color."linear-gradient" — { kind, angle, stops }. angle is a number 0–360 (degrees). stops is an array of 2–8 stops, each { color, at? } where color is a ref-or-color and at is an optional percentage position (number or "NN%" string)."radial-gradient" — { kind, shape?, position?, stops }. shape is optional, "circle" or "ellipse". position is optional { x, y } where each is a percentage. stops is the same 2–8 stop array as linear."pattern" — { kind, pattern, color, size?, opacity? }. pattern is one of "grid" | "scanlines" | "dots" | "diagonal-lines". color is a ref-or-color. size is an optional length. opacity is an optional number 0–1."blur-orb" — { kind, color, x, y, size, blur?, opacity? }. color is a ref-or-color. x/y are percentages (orb center). size is a length. blur is an optional length. opacity is an optional number 0–1.border — optional { color, width? }. color is a ref-or-color; width is an optional length.The layer list is capped at 8 and each layer is a fixed, closed shape — this is not free-form CSS.
A theme is checked in stages when you save it:
semanticTokens reference is followed to a real color. Cycles, dangling references, and chains deeper than 8 are rejected.Every color, length, and shadow value must come from a closed allowed set: no raw CSS strings, no CSS shorthand, no font-family lists. Theme values become CSS shown to every player, so the value grammar is restricted for safety. An invalid theme is rejected on save, with the offending field identified.
{
"version": 1,
"fonts": { "heading": "Heading", "body": "Body", "mono": "Mono" },
"colors": {
"accent": { "50": "#fef6e3", "500": "#ef9f27", "900": "#412402" },
"neutral": { "800": "#2c2c2a" },
"surface": { "50": "#fffdf8", "100": "#fdf6e8", "200": "#faeeda" },
"surfaceDark": { "300": "#1f2046", "500": "#14152e" }
},
"semanticTokens": {
"bg": {
"canvas": { "light": "surface.50", "dark": "surfaceDark.500" },
"surface": { "light": "#ffffff", "dark": "surfaceDark.300" }
},
"fg": {
"default": { "light": "neutral.800", "dark": "surface.200" },
"muted": { "light": "neutral.800", "dark": "rgba(250,238,218,0.78)" }
}
},
"radii": { "xs": "2px", "md": "4px", "2xl": "16px" },
"shadows": {
"none": "none",
"sm": [{ "offsetX": 0, "offsetY": 1, "blur": 2, "color": "rgba(0,0,0,0.05)" }]
},
"palettes": {},
"spacing": { "1": "4px", "2": "8px", "0.5": "2px" }
}
background{
"version": 1,
"fonts": { "heading": "Heading", "body": "Body", "mono": "Mono" },
"colors": {
"accent": { "500": "#ef9f27" },
"neutral": { "800": "#2c2c2a" },
"surface": { "50": "#fffdf8" },
"surfaceDark": { "500": "#14152e", "900": "#080919" }
},
"semanticTokens": {
"bg": {
"canvas": { "light": "surface.50", "dark": "surfaceDark.500" }
},
"fg": {
"default": { "light": "neutral.800", "dark": "surface.50" }
}
},
"radii": { "md": "4px" },
"shadows": { "none": "none" },
"palettes": {},
"spacing": { "1": "4px", "2": "8px" },
"background": {
"light": {
"baseColor": "surface.50",
"layers": [
{
"kind": "linear-gradient",
"angle": 180,
"stops": [
{ "color": "surface.50", "at": "0%" },
{ "color": "accent.500", "at": "100%" }
]
}
]
},
"dark": {
"baseColor": "surfaceDark.900",
"layers": [
{
"kind": "linear-gradient",
"angle": 180,
"stops": [
{ "color": "surfaceDark.900", "at": "0%" },
{ "color": "surfaceDark.500", "at": "100%" }
]
},
{
"kind": "blur-orb",
"color": "accent.500",
"x": "30%",
"y": "20%",
"size": "320px",
"blur": "80px",
"opacity": 0.35
}
]
}
}
}