Have more questions? Join our

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.

Game themes

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.

Top-level shape

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.

Reference grammar

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.

Validation

A theme is checked in stages when you save it:

  1. Structure — shape and required fields. The top-level object is strict, so unknown top-level keys are rejected here.
  2. References — every semanticTokens reference is followed to a real color. Cycles, dangling references, and chains deeper than 8 are rejected.
  3. Values — every resolved color, length, and shadow value is re-checked against the allowed value set. A semantic-token chain that bottoms out at a non-color string (e.g. a color token that resolved to a spacing value) fails here.

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.

Minimal example

{
  "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" }
}

Example with a 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
        }
      ]
    }
  }
}