Have more questions? Join our Discord

Design the theme

The theme is /theme.json: the game's colors, light and dark modes, radii, shadows, spacing and page background. The rules pages and the marketing page use it through $ tokens. The client reads it with useGameTheme. Follow these steps in order.

1. Choose the colors

Write the raw colors in colors, as palettes of shades:

"colors": {
  "neutral": { "50": "#f4f4f2", "800": "#232323" },
  "accent": { "500": "#4d6a7c" }
}

Palette and token names start with a letter or _, then letters, digits and _. Hyphens are not allowed. Shade keys use letters, digits and _.

Done when every color the game uses is a shade in colors.

2. Name each color by its job

semanticTokens names what each color is for, with a value for light mode and one for dark mode. A value is a color or a reference to a shade, written palette.shade without $:

"semanticTokens": {
  "text": { "primary": { "light": "neutral.800", "dark": "neutral.50" } },
  "bg": { "surface": { "light": "#ffffff", "dark": "#1b1b1f" } }
}

On the pages, only semantic tokens switch between light and dark. Every other token is fixed.

Done when every text, background and border color on the pages is a semantic token with both modes.

3. Write the whole file

Write /theme.json with write_file. It must be exactly that path. All fields:

Field Required Holds
version yes 1
fonts yes { "heading", "body", "mono" }, each sans, serif or mono
colors yes palettes of shades
semanticTokens yes groups of { light, dark } tokens
radii yes named lengths, such as { "md": 8 }
shadows yes named shadows: "none" or up to 4 { offsetX, offsetY, blur?, spread?, color, inset? }
palettes yes more named color sets, referenced as palettes.name.key
spacing yes named lengths, such as { "4": 16 }
background no { light, dark }, both required: each { baseColor, layers, border? }
defaultColorMode no light or dark. Without it, the pages follow the visitor's setting.
mood, voice no short words that describe the game's style

A background layer is solid, linear-gradient, radial-gradient, pattern (grid, scanlines, dots, diagonal-lines) or blur-orb. With dark semantic tokens, also set background.dark, so dark text does not sit on a white page.

write_file refuses a file that is not JSON (INVALID_THEME_JSON) or does not fit (INVALID_THEME), and says which field is wrong.

Done when write_file accepts /theme.json.

4. Use it on the pages

In a page style, a theme value is a $ token:

Token Reads
$semanticTokens.text.primary a semantic token, which follows light and dark
$colors.accent.500 a shade
$palettes.name.key a palette entry
$radii.md, $shadows.card, $spacing.4 a named value

A token that does not exist is dropped without an error. Set fontFamily to sans, serif or mono directly.

Done when validate_code answers no page errors and the pages read well in both modes.

5. Use it in the client

useGameTheme() gives the theme as written, with references not resolved. useColorMode() gives "light" or "dark", and changes when the player changes their setting. Resolve a semantic token in the client:

import { useColorMode, useGameTheme, type GameTheme } from "boardweaver/react";

function resolveColor(theme: GameTheme, value: string): string {
  const [palette, shade] = value.split(".");
  return theme.colors[palette]?.[shade] ?? value;
}

export function useToken(group: string, token: string): string | undefined {
  const theme = useGameTheme();
  const mode = useColorMode();
  const pair = theme.semanticTokens[group]?.[token];
  return pair && resolveColor(theme, pair[mode]);
}

The platform paints no background behind the board: the client paints its own.

Done when the board uses the theme's colors in both modes.

6. Commit

Done when commit succeeds.