Token structure
Source files
Section titled “Source files”| 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.
Three tiers of color
Section titled “Three tiers of color”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.
Themes
Section titled “Themes”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 |
mise run theme # list the themesmise run theme ocean # apply one and regenerate the platform filesEvery 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.
# 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.
Typography
Section titled “Typography”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-allon the web andlineBreakStrategyIOSon 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.
Other tokens
Section titled “Other tokens”| 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 |
What the build rejects
Section titled “What the build rejects”- 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 inms, 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)