Themes
A theme sets only its inputs: a hue for light and for dark, and optionally its two fonts and its neutrals. Every other token is core and follows from them, so every theme keeps the same shapes, spacing, states, and status colours. The family's sites each have a theme in their own hue, and open palettes such as Catppuccin and GitHub's Primer map onto the same inputs. The tests measure each theme's contrast in both modes.
Each preview renders in one theme and one mode, and its label says which. The page around the previews is Graphite Indigo unless you choose another theme below, which loads that theme's file after tokens.css, as an app would.
Graphite Indigo
DefaultThe default: an indigo hue on graphite neutrals.
- Hue
- Light
#4b45d1Dark#a5a1ff - Neutrals
- Core
- Fonts
- IBM Plex Sans for textIBM Plex Mono for code
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass5.34:1 | Pass5.05:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass5.34:1 | Pass5.05:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass6.61:1 | Pass5.14:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass6.26:1 | Pass8.31:1 |
The $ prompt on the command plateNeeds 3:1 | Pass6.98:1 | Pass7.48:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass5.65:1 | Pass5.43:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.96:1 | Pass4.29:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass6.12:1 | Pass6.12:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass5.97:1 | Pass8.31:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass4.76:1 | Pass4.81:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass4.75:1 | Pass7.37:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass10.29:1 | Pass11.03:1 |
Catppuccin
Catppuccin's open palette (@catppuccin/palette 1.8.0, MIT): Latte in light and Mocha in dark, with Mauve as the hue. Base is the raised surface in both modes; Latte has nothing lighter, so base is also its page. Muted text is Subtext 1 in Latte, where Subtext 0 misses 4.5:1.
- Hue
- Light
#8839efDark#cba6f7 - Neutrals
- LightDark
- Fonts
- Same as Graphite Indigo
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass3.93:1 | Pass5.31:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass4.79:1 | Pass5.31:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass4.79:1 | Pass5.59:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass4.79:1 | Pass9.23:1 |
The $ prompt on the command plateNeeds 3:1 | Pass7.61:1 | Pass8.30:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass4.71:1 | Pass5.65:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.25:1 | Pass4.44:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass6.72:1 | Pass6.72:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass5.87:1 | Pass8.02:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass4.68:1 | Pass4.50:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass4.66:1 | Pass7.18:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass9.92:1 | Pass10.82:1 |
Copper
The devps site's orange, a step darker in light so a selected row reads at 4.5:1: a burnt orange in light and a soft apricot in dark.
- Hue
- Light
#b04003Dark#f39a62 - Neutrals
- Same as Graphite Indigo
- Fonts
- Same as Graphite Indigo
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass4.60:1 | Pass5.30:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass4.60:1 | Pass5.30:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass5.67:1 | Pass5.41:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass5.37:1 | Pass8.74:1 |
The $ prompt on the command plateNeeds 3:1 | Pass7.17:1 | Pass7.82:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass5.65:1 | Pass5.43:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.96:1 | Pass4.29:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass6.41:1 | Pass6.41:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass5.97:1 | Pass8.31:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass4.76:1 | Pass4.81:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass4.75:1 | Pass7.37:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass10.06:1 | Pass10.96:1 |
Fern
The wtree site's green, a step darker in light so a selected row reads at 4.5:1: a leaf green in light and a pale green in dark.
- Hue
- Light
#27752aDark#7fd083 - Neutrals
- Same as Graphite Indigo
- Fonts
- Same as Graphite Indigo
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass4.53:1 | Pass5.89:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass4.56:1 | Pass5.89:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass5.54:1 | Pass6.33:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass5.25:1 | Pass10.23:1 |
The $ prompt on the command plateNeeds 3:1 | Pass8.24:1 | Pass8.92:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass5.65:1 | Pass5.43:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.96:1 | Pass4.29:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass7.17:1 | Pass7.17:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass5.97:1 | Pass8.31:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass4.76:1 | Pass4.81:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass4.75:1 | Pass7.37:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass9.86:1 | Pass10.68:1 |
GitHub
GitHub's open Primer palette and font stacks (@primer/primitives 11.10.0, MIT), light and dark. The hue is GitHub's green: the primary button in light, and the success foreground in dark, since this system's dark buttons take a dark label. The page is Primer's default background, and surfaces its muted one.
- Hue
- Light
#1f883dDark#3fb950 - Neutrals
- LightDark
- Fonts
- Mona Sans VF for textui-monospace for code
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass3.78:1 | Pass4.77:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass4.52:1 | Pass4.84:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass4.52:1 | Pass4.77:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass4.52:1 | Pass7.45:1 |
The $ prompt on the command plateNeeds 3:1 | Pass5.76:1 | Pass6.89:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass5.18:1 | Pass4.82:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.24:1 | Pass3.26:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass5.71:1 | Pass5.71:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass6.70:1 | Pass8.47:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass5.35:1 | Pass4.91:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass5.33:1 | Pass7.08:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass9.38:1 | Pass11.23:1 |
Iris
The filip-stack site's violet: a saturated violet in light and a lilac in dark.
- Hue
- Light
#6d3fd6Dark#b49cff - Neutrals
- Same as Graphite Indigo
- Fonts
- Same as Graphite Indigo
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass4.94:1 | Pass5.06:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass4.94:1 | Pass5.06:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass6.09:1 | Pass5.14:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass5.77:1 | Pass8.31:1 |
The $ prompt on the command plateNeeds 3:1 | Pass6.88:1 | Pass7.48:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass5.65:1 | Pass5.43:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.96:1 | Pass4.29:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass6.13:1 | Pass6.13:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass5.97:1 | Pass8.31:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass4.76:1 | Pass4.81:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass4.75:1 | Pass7.37:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass10.14:1 | Pass11.03:1 |
Lagoon
The annoterm site's teal, a step darker in light so a selected row reads at 4.5:1: a deep teal in light and a soft aqua in dark.
- Hue
- Light
#087266Dark#5cc8b8 - Neutrals
- Same as Graphite Indigo
- Fonts
- Same as Graphite Indigo
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass4.60:1 | Pass5.57:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass4.61:1 | Pass5.57:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass5.63:1 | Pass5.86:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass5.33:1 | Pass9.46:1 |
The $ prompt on the command plateNeeds 3:1 | Pass7.67:1 | Pass8.36:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass5.65:1 | Pass5.43:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.96:1 | Pass4.29:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass6.77:1 | Pass6.77:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass5.97:1 | Pass8.31:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass4.76:1 | Pass4.81:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass4.75:1 | Pass7.37:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass9.94:1 | Pass10.83:1 |
Lapis
The Workbench site's blue: a cobalt in light and a periwinkle in dark.
- Hue
- Light
#2b59d6Dark#8aa8ff - Neutrals
- Same as Graphite Indigo
- Fonts
- Same as Graphite Indigo
pnpm tokens:buildpnpm tokens:buildContrast
| Check | Light | Dark |
|---|---|---|
| Focus ring on the page, a surface, a raised surface, and the fills an inset ring sits onNeeds 3:1 | Pass4.73:1 | Pass5.04:1 |
| Current row text on its fill, on each surface, and a current link on the pageNeeds 4.5:1 | Pass4.73:1 | Pass5.04:1 |
| Selected segmented label on its thumbNeeds 4.5:1 | Pass5.80:1 | Pass5.14:1 |
| Primary label on its fill, at rest and on hoverNeeds 4.5:1 | Pass5.49:1 | Pass8.30:1 |
The $ prompt on the command plateNeeds 3:1 | Pass6.78:1 | Pass7.47:1 |
| Text and muted text on every fill that carries them, at rest, on hover, and pressedNeeds 4.5:1 | Pass5.65:1 | Pass5.43:1 |
| Control edge on the page, a surface, and a raised surfaceNeeds 3:1 | Pass3.96:1 | Pass4.29:1 |
| Highlighted text in a terminal capture, and its current tabNeeds 4.5:1 | Pass6.12:1 | Pass6.12:1 |
| Link text on the page, a surface, and a raised surfaceNeeds 4.5:1 | Pass5.97:1 | Pass8.31:1 |
| Status text on the page, a surface, and a raised surface, on its badge, and on a danger buttonNeeds 4.5:1 | Pass4.76:1 | Pass4.81:1 |
| Syntax colours on a code blockNeeds 4.5:1 | Pass4.75:1 | Pass7.37:1 |
| Text and captions on the command plateNeeds 4.5:1 | Pass10.02:1 | Pass11.03:1 |
Use a theme
tokens.css carries the default theme. To use another, load its theme file after tokens.css. It sets the theme's inputs on :root, with dark blocks for the values that differ in dark, so light and dark both follow.
@import "@filipgutica/design-system/tokens.css";@import "@filipgutica/design-system/themes/lapis.css";Load a theme on :root only. The tokens that follow the hue resolve there, so a theme scoped to an element below :root changes the hue there, but the accent, focus ring, and selection keep the default theme's colours.
Each theme's source also ships as themes/<id>.tokens.json, a W3C Design Tokens file, for design-token tools such as Style Dictionary and Tokens Studio.
Import a VS Code theme
@filipgutica/design-system/theme-mapper is experimental. mapVsCodeTheme maps a parsed VS Code colour theme, with its include already merged, onto theme inputs. It selects the theme's own colours and never alters one. problems lists each check that still fails: a theme problem is in the theme's colours, and a core problem is a core colour, such as a link, status, or syntax colour, on the theme's fills, which the theme cannot fix. Each problem lists the inks and fills that fail.
A VS Code theme has one mode, so lock the page to it: set the variables from themeVariables inline on <html>, set data-theme to the theme's appearance, and lock the mode toggle while the theme is on. A :root rule would lose to the dark blocks in tokens.css.
import { mapVsCodeTheme, themeVariables } from "@filipgutica/design-system/theme-mapper";const mapped = mapVsCodeTheme(vsCodeTheme);const root = document.documentElement;root.dataset.theme = mapped.appearance;for (const [name, value] of Object.entries(themeVariables(mapped))) root.style.setProperty(name, value);Make a theme
Add a .tokens.json file to tokens/themes/ with only the inputs, then build. A theme file uses the W3C Design Tokens format (2025.10): a light and a dark group of sRGB colour tokens, a $description for its note, and its name under $extensions. A theme sets a hue for light and dark, and may name its own font stacks and its own neutrals: the page, surfaces, hover, edges, and text, for both modes. That is how an open palette, such as Catppuccin or GitHub's Primer, becomes a theme. Everything else is core. Where the hue cannot carry the text on a selected fill, the build takes the theme's text colour, then black or white. The tests check every theme against the contrast checks above, in both modes.
{ "$description": "The Workbench site's blue: a cobalt in light and a periwinkle in dark.", "$extensions": { "io.github.filipgutica.design-system": { "name": "Lapis" } }, "light": { "$type": "color", "color-project": { "$value": { "colorSpace": "srgb", "components": [0.1686, 0.349, 0.8392], "hex": "#2b59d6" } } }, "dark": { "$type": "color", "color-project": { "$value": { "colorSpace": "srgb", "components": [0.5412, 0.6588, 1], "hex": "#8aa8ff" } } }}pnpm tokens:build