Skip to content

Token structure

File Contents
palette.json Six raw color ramps referenced by other tokens. Excluded from generated platform code. The accent and neutral ramps come from the active theme
color.light.json, color.dark.json Semantic colors and component colors. Every token name must exist in both modes
base.json Space, radius, border, focus, type, motion, and shadow tokens, plus the scrim color
size.pointer.json, size.touch.json Control sizes per input device

Token files use the DTCG 2025.10 format, with separate files for each color mode and input device. Although DTCG specifies a Resolver module for describing multi-file sets like these, tool support is still limited, so it is not used here. Every color must be in sRGB: neither GPUI nor React Native supports wider color gamuts, so wide-gamut tokens would render inconsistently across platforms.

flowchart LR
P["palette<br/>accent.600"] --> S["semantic<br/>accent.default"] --> C["component<br/>button.primary.background"]
Tier Example Used by
Palette palette.accent.600 Referenced only by semantic colors; omitted from generated files
Semantic text.secondary, border.strong, status.danger Application screens and components
Component button.primary.background-hover, field.border-invalid That specific component across every platform

Keeping a dedicated component tier means questions like “what color is a pressed primary button?” are answered once in the token files. Without that tier, each platform ends up making its own choice in code, causing implementations to drift apart.

Semantic color groups:

Group Tokens Use
background base, surface, raised, disabled Page backgrounds, cards, floating surfaces, disabled controls
text primary, secondary, tertiary, disabled, accent, on-accent, on-danger Text colors (on-* is used for text drawn on a colored fill). tertiary is used for placeholders, timestamps, and marks such as chevrons; it maintains 4.5:1 contrast on the page and on surfaces, and is not used on raised surfaces
border default, control, strong, focus Decorative dividers; outlines on controls already identified by a label; control boundaries that require 3:1 contrast; focus rings
accent default, hover, pressed, subtle Primary view actions and selected states
control default, hover, pressed Neutral control fills
danger default, hover, pressed Destructive action fills
status danger, success, warning, info, plus a subtle variant of each Status text and its matching tinted background

Interactive state colors (hover and pressed) are defined as explicit solid values rather than alpha overlays. GPUI and React Native cannot blend colors at runtime, and explicit values allow the build script to check contrast statically.

In light mode, the page background is slightly off-white while surfaces are pure white, allowing cards to stand out with just a hairline border and a subtle shadow. Rather than simply inverting the light palette, dark mode adjusts how elevation and contrast work: floating surfaces are distinguished by a lighter background fill instead of a shadow, and accent fills shift to a ramp step that maintains 4.5:1 contrast against white text.

A theme is a JSON file in design/themes containing two ramps: accent and neutral. Semantic colors reference the palette by step (accent.600, neutral.100), so swapping those two ramps recolors the entire system without touching any semantic or component tokens.

Theme Accent Neutrals
indigo (default) Indigo Cool grey
ocean Blue Slate
forest Deep green Sage
sunset Burnt orange Warm stone
plum Magenta Mauve
Terminal window
mise run theme # list the themes
mise run theme ocean # apply one and regenerate the platform files

Every theme passes all contrast checks, and the test suite enforces this. tools/design-build/src/make-theme.ts constructs each ramp by matching the relative luminance of the corresponding step in indigo while changing only hue and chroma. Because WCAG contrast depends solely on relative luminance, all contrast ratios carry over unchanged.

Terminal window
# A new theme: an accent hue in OKLCH degrees, and optionally how vivid it is.
node tools/design-build/src/make-theme.ts coral --hue 25 --chroma 0.9 --description "A coral accent."

The status ramps (red, green, amber, blue) remain fixed across themes so that warnings and errors look consistent regardless of the active accent color.

Screens select a typographic role rather than a raw font size. Each role bundles font size, weight, line height, and letter spacing together.

Role Size / line height / weight Use
display 28 / 1.25 / 700 Top-level view title, used at most once per screen
title 22 / 1.25 / 700 Section and dialog titles
heading 18 / 1.4 / 600 Card titles
body, body-strong 16 / 1.5 / 400, 600 Body copy and inline emphasis
label 14 / 1.4 / 500 Button and form field labels
detail 14 / 1.5 / 400 Helper descriptions and secondary text
footnote 12 / 1.4 / 500 Badges and timestamps
  • Body text is set at 16px, preventing iOS Safari from auto-zooming the page when a text input receives focus.
  • The minimum font size is 12px, since Hangul glyphs become indistinct below that size.
  • Body copy uses a 1.5 line height to give Hangul the extra vertical clearance it requires compared to Latin text at the same point size.
  • Korean text wraps at word boundaries via word-break: keep-all on the web and lineBreakStrategyIOS on iOS.

The font stack lists Pretendard first, followed by the system Latin fonts and then the system Korean fonts. This order matters when Pretendard is not loaded: Latin text falls back to the system font rather than the Latin glyphs of a Korean font. Because font files are not bundled in this repository, each platform renders with the system font unless the application loads Pretendard itself. To change the typeface, update font.family.sans in base.json.

On the web, the Tailwind configuration generates utility classes for typographic roles (such as text-acme-body) and omits utilities for raw font sizes.

Token Pointer Touch
size.control.sm 28 40
size.control.md 36 48
size.control.lg 44 56
size.target-min 24 48
size.switch.width, size.switch.height 36 × 20 52 × 32
size.checkbox 18 24
size.list-row 40 56
size.bar 48 56
size.button-radius 10 capsule
size.icon.sm, md, lg 16, 20, 24 16, 20, 24
size.avatar.sm, md, lg 24, 32, 40 24, 32, 40
size.spinner.sm, md, lg 16, 24, 32 16, 24, 32
size.badge 24 24
size.progress 8 8

On the web, @media (pointer: coarse) switches between the pointer and touch scales. Keying off the input device rather than viewport width avoids common edge cases: a narrow browser window on a laptop is still used with a mouse, and a large tablet is still used with touch. The GPUI desktop app always uses pointer sizes, the React Native app always uses touch sizes, and the Compose client uses touch sizes on Android and iOS and pointer sizes on desktop.

Even when a control is visually smaller than target-min, its hit area expands to meet the minimum target size. On mobile, a small button renders at 40px tall while accepting touches across a 48px height.

design/icons holds one SVG file per icon, drawn on a 24 by 24 grid with a 2px stroke and no fill. The 26 icons included with the template come from Lucide (under the ISC license in design/icons/LICENSE). To add an icon, place its SVG file in that directory and run mise run tokens.

The generator converts each shape to path data and writes it into the generated platform files, so no platform parses SVG at runtime and no icon font or asset bundle is needed:

Output Contents
TypeScript icons, a map from icon name to path data, and the IconName type. The web and React Native Icon components render these paths; React Native uses react-native-svg
Kotlin The IconName enum, where each entry carries its path data. The Compose Icon component strokes the paths on a canvas
Rust The Icon enum with name() and svg() methods. The icon function passes the SVG document to GPUI

The build rejects any file that is not on the 24 by 24 grid, does not use a kebab-case name, or contains a shape that cannot be converted to a path.

Group Values Rule
space 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64 4px grid; each token name corresponds to its multiple of 4. 0-5, 1-5, and 2-5 are half steps used inside controls
radius sm 6, md 10, lg 16, xl 24, full Inner control elements, standard controls, containers, and floating surfaces detached from screen edges (sheets)
border.width thin 1, thick 2 thick is reserved for states that cannot rely on color alone
focus width 2, offset 2 The 2px offset separates the focus ring from the control’s fill
motion.duration fast 120, normal 200, slow 320 Press feedback, elements appearing, and large surface transitions
motion.easing standard, enter, exit On-screen movement, entering elements, and exiting elements
motion.press-scale 0.97 Scale factor applied to a button while pressed
motion.spring default 700 / 0.9, fast 1400 / 0.9 Spring stiffness and damping ratio used by React Native and Compose for presses, switches, and touch-driven motion. Web and GPUI use the duration and easing tokens instead
shadow subtle, raised, overlay Cards and outlined controls, floating bars and menus, and modal sheets. Because shadows are barely visible in dark mode, dark surfaces rely on lighter background fills to convey elevation
  • Colors outside the sRGB gamut, or colors whose hex string does not match their RGB channel values
  • Dimensions not specified in px, durations not specified in ms, or inset shadows
  • Tokens defined in only one color mode or only one input device file
  • Color pairings that fall below their required WCAG contrast ratio (the error message reports the pairing name, color mode, and measured ratio)