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.