Skip to content

Design Tokens

Karthik P edited this page Oct 24, 2026 · 1 revision

This page is the single source of truth for every design value on the website: colour, type, space, radius, shadow, motion, layout and stacking order. It gives the tokens in the W3C Design Tokens format, the CSS custom properties they become, the Tailwind v4 @theme block and the mapping onto Fumadocs UI's variables.

Other pages in Design System explain when to use each token. If a value elsewhere disagrees with this page, this page wins and the other page is a bug.

How a change flows

  1. Edit the token in website/tokens/, using a value from a mockup the owner approved, and record the choice in the decision log on Design System.
  2. Copy the value into website/app/global.css in the same pull request. CSS names match token names one to one (color.text-2 → --wc-text-2).
  3. Tailwind utilities and Fumadocs UI both read the CSS variables, so components need no change.
  4. Brand files made in Canva use the same hex values, typed in by hand (Brand).

Nothing converts the JSON to CSS: a converter such as Style Dictionary would be a new dependency. Proposed: a dependency-free Node script, website/scripts/check-tokens.mjs, that resolves the JSON and fails CI if global.css disagrees.

Files

File Holds
website/tokens/base.tokens.json Primitive colours, type, space, radius, motion, layout, z-index; the same in light and dark
website/tokens/light.tokens.json Semantic colours and the popover shadow, light
website/tokens/dark.tokens.json The same names, dark
website/tokens/workclock.resolver.json Merges base with light or dark
website/app/global.css The CSS the site loads (Fumadocs' default location)

The four tokens/ paths are Proposed. The JSON follows the Design Tokens Format Module 2025.10, with colours as in the Color module and light/dark as a "theme" modifier of the Resolver module.

Naming

Kind Pattern Example Used in components
Primitive colour palette.<hue>.<step> palette.neutral.920 Never; primitives only feed semantic tokens
Semantic colour color.<role> color.text-2 Always, as var(--wc-text-2) or text-text-2
Everything else <group>.<name> radius.lg, duration.base Through the CSS variable or utility below

The colour step is (100 − CIELAB L*) × 10, rounded to the nearest 5: neutral.0 is white, neutral.960 the darkest grey, and a new grey slots in by its lightness.

Colour tokens

Roles and contrast ratios are on Color.

Token Light Dark CSS variable Tailwind utility
color.bg #FFFFFF #0B0E14 --wc-bg bg-bg
color.surface #F6F7F9 #121620 --wc-surface bg-surface
color.raised #FFFFFF #1A1F2B --wc-raised bg-raised
color.rule #E3E6EB #242B37 --wc-rule border-rule
color.rule-strong #CDD2DA #343C4A --wc-rule-strong border-rule-strong
color.text #0F1729 #E8ECF2 --wc-text text-text
color.text-2 #4A5363 #A3ACBA --wc-text-2 text-text-2
color.text-3 #6B7484 #8A93A3 --wc-text-3 text-text-3, border-text-3
color.work #DBE2EC #1D2532 --wc-work bg-work, fill-work
color.used = text = text --wc-used bg-used, fill-used
color.accent #C27C00 #FFB224 --wc-accent bg-accent, border-accent, fill-accent (graphics only)
color.accent-text #8F5A00 #FFB224 --wc-accent-text text-accent-text
color.focus #1F5BD8 #7AA2FF --wc-focus outline-focus
color.link = text = text --wc-link global a style
color.link-underline = rule-strong = rule-strong --wc-link-underline global a style
color.link-underline-hover = accent = accent --wc-link-underline-hover global a style
color.success #0E7A55 #4CC38A --wc-success text-success
color.danger #C8331F #FF6B5A --wc-danger text-danger
color.info #1F5BD8 #7AA2FF --wc-info text-info
color.warning = accent = accent --wc-warning border-warning
color.warning-text = accent-text = accent-text --wc-warning-text text-warning-text
color.overlay rgb(15 23 41 / 0.2) rgb(0 0 0 / 0.5) --wc-overlay through Fumadocs (--color-fd-overlay)
color.logo-dot #D98A00 #FFB224 none Logo files only, never CSS

color.overlay is the dimmed layer behind the search dialog and other dialogs.

Other tokens

Group Values CSS / Tailwind Details
font.family sans Instrument Sans, mono JetBrains Mono --font-sans, --font-mono Typography
font.weight 400, 500, 600 font-normal, font-medium, font-semibold Typography
font.size 12, 13, 14, 16, 18, 20, 24, 30, 38, 48, 60 px role utilities such as text-body, text-h1 Typography
font.leading 1.1, 1.25, 1.3, 1.5, 1.6, 1.7 set with each --text-* size Typography
font.tracking −0.035, −0.03, −0.02, −0.01, 0 em set with each --text-* size Typography
space 0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96 px Tailwind steps 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24 Layout and Spacing
radius none 0, sm 4, md 6, lg 8, xl 12, full rounded-none … rounded-xl, rounded-full (Tailwind's defaults) Shape Elevation and Motion
border.width 1 px border Shape Elevation and Motion
shadow.popover in the theme files --wc-shadow-popover → shadow-popover Shape Elevation and Motion
duration fast 100, base 160, slow 240 ms --wc-duration-*; duration-100, duration-160, duration-240 Shape Elevation and Motion
easing.standard cubic-bezier(0.2, 0, 0, 1) --wc-ease, ease-standard Shape Elevation and Motion
layout header 64, sidebar 280, prose 640, toc 220, page 1400, landing 1200, gutter 16 px --wc-header … --wc-gutter Layout and Spacing
z base 0, sticky 10, sidebar 20, header 30, drawer 40, popover 50, skip-link 60 z-0 … z-60 Layout and Spacing

Token files

Values match the tables above. Hex values are lowercase in the JSON, as in the Color module's examples.

workclock.resolver.json

{
  "$schema": "https://www.designtokens.org/schemas/2025.10/resolver.json",
  "version": "2025.10",
  "name": "workclock website",
  "sets": {
    "base": { "sources": [{ "$ref": "base.tokens.json" }] }
  },
  "modifiers": {
    "theme": {
      "description": "Light or dark: the visitor's OS setting, or the toggle",
      "contexts": {
        "light": [{ "$ref": "light.tokens.json" }],
        "dark": [{ "$ref": "dark.tokens.json" }]
      },
      "default": "light"
    }
  },
  "resolutionOrder": [
    { "$ref": "#/sets/base" },
    { "$ref": "#/modifiers/theme" }
  ]
}

light.tokens.json

{
  "color": {
    "$type": "color",
    "bg": {"$value": "{palette.neutral.0}", "$description": "Page"},
    "surface": {"$value": "{palette.neutral.30}", "$description": "Active sidebar item, code blocks, demo frame"},
    "raised": {"$value": "{palette.neutral.0}", "$description": "Popovers, search dialog"},
    "rule": {"$value": "{palette.neutral.90}", "$description": "Hairline borders, dividers, grid lines"},
    "rule-strong": {"$value": "{palette.neutral.160}", "$description": "Focused table rows, link underlines, hover borders"},
    "text": {"$value": "{palette.neutral.920}", "$description": "Body and headings"},
    "text-2": {"$value": "{palette.neutral.650}", "$description": "Secondary text, nav"},
    "text-3": {"$value": "{palette.neutral.515}", "$description": "Captions, placeholders, form-control borders"},
    "work": {"$value": "{palette.neutral.105}", "$description": "Working-hour bars"},
    "used": {"$value": "{color.text}", "$description": "Counted time in diagrams"},
    "accent": {"$value": "{palette.amber.420}", "$description": "Due marker, active tab, current page, warning callouts; graphics only"},
    "accent-text": {"$value": "{palette.amber.570}", "$description": "Amber that must be read as text"},
    "focus": {"$value": "{palette.blue.575}", "$description": "Focus rings"},
    "link": {"$value": "{color.text}"},
    "link-underline": {"$value": "{color.rule-strong}"},
    "link-underline-hover": {"$value": "{color.accent}"},
    "success": {"$value": "{palette.green.550}"},
    "danger": {"$value": "{palette.red.545}", "$description": "Errors, danger callouts"},
    "info": {"$value": "{palette.blue.575}", "$description": "Note callouts"},
    "warning": {"$value": "{color.accent}", "$description": "Warning callout bar and icon"},
    "warning-text": {"$value": "{color.accent-text}"},
    "overlay": {"$value": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "alpha": 0.2, "hex": "#0f1729"}, "$description": "Behind dialogs"},
    "logo-dot": {"$value": "{palette.amber.360}", "$description": "Logo files only"}
  },
  "shadow": {
    "$type": "shadow",
    "popover": {"$value": [{"color": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "alpha": 0.06, "hex": "#0f1729"}, "offsetX": {"value": 0, "unit": "px"}, "offsetY": {"value": 1, "unit": "px"}, "blur": {"value": 2, "unit": "px"}, "spread": {"value": 0, "unit": "px"}}, {"color": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "alpha": 0.08, "hex": "#0f1729"}, "offsetX": {"value": 0, "unit": "px"}, "offsetY": {"value": 8, "unit": "px"}, "blur": {"value": 24, "unit": "px"}, "spread": {"value": 0, "unit": "px"}}], "$description": "Popovers only"}
  }
}

dark.tokens.json

{
  "color": {
    "$type": "color",
    "bg": {"$value": "{palette.neutral.960}", "$description": "Page"},
    "surface": {"$value": "{palette.neutral.925}", "$description": "Active sidebar item, code blocks, demo frame"},
    "raised": {"$value": "{palette.neutral.880}", "$description": "Popovers, search dialog"},
    "rule": {"$value": "{palette.neutral.825}", "$description": "Hairline borders, dividers, grid lines"},
    "rule-strong": {"$value": "{palette.neutral.750}", "$description": "Focused table rows, link underlines, hover borders"},
    "text": {"$value": "{palette.neutral.65}", "$description": "Body and headings"},
    "text-2": {"$value": "{palette.neutral.300}", "$description": "Secondary text, nav"},
    "text-3": {"$value": "{palette.neutral.395}", "$description": "Captions, placeholders, form-control borders"},
    "work": {"$value": "{palette.neutral.855}", "$description": "Working-hour bars"},
    "used": {"$value": "{color.text}", "$description": "Counted time in diagrams"},
    "accent": {"$value": "{palette.amber.220}", "$description": "Due marker, active tab, current page, warning callouts; graphics only"},
    "accent-text": {"$value": "{palette.amber.220}", "$description": "Amber that must be read as text"},
    "focus": {"$value": "{palette.blue.325}", "$description": "Focus rings"},
    "link": {"$value": "{color.text}"},
    "link-underline": {"$value": "{color.rule-strong}"},
    "link-underline-hover": {"$value": "{color.accent}"},
    "success": {"$value": "{palette.green.290}"},
    "danger": {"$value": "{palette.red.360}", "$description": "Errors, danger callouts"},
    "info": {"$value": "{palette.blue.325}", "$description": "Note callouts"},
    "warning": {"$value": "{color.accent}", "$description": "Warning callout bar and icon"},
    "warning-text": {"$value": "{color.accent-text}"},
    "overlay": {"$value": {"colorSpace": "srgb", "components": [0, 0, 0], "alpha": 0.5, "hex": "#000000"}, "$description": "Behind dialogs"},
    "logo-dot": {"$value": "{palette.amber.220}", "$description": "Logo files only"}
  },
  "shadow": {
    "$type": "shadow",
    "popover": {"$value": [{"color": {"colorSpace": "srgb", "components": [0, 0, 0], "alpha": 0.5, "hex": "#000000"}, "offsetX": {"value": 0, "unit": "px"}, "offsetY": {"value": 8, "unit": "px"}, "blur": {"value": 24, "unit": "px"}, "spread": {"value": 0, "unit": "px"}}], "$description": "Popovers only; dark popovers also keep their 1px rule border"}
  }
}

base.tokens.json

Show the file (146 lines)
{
  "palette": {
    "$type": "color",
    "neutral": {
      "0": {"$value": {"colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff"}},
      "30": {"$value": {"colorSpace": "srgb", "components": [0.9647, 0.9686, 0.9765], "hex": "#f6f7f9"}},
      "65": {"$value": {"colorSpace": "srgb", "components": [0.9098, 0.9255, 0.949], "hex": "#e8ecf2"}},
      "90": {"$value": {"colorSpace": "srgb", "components": [0.8902, 0.902, 0.9216], "hex": "#e3e6eb"}},
      "105": {"$value": {"colorSpace": "srgb", "components": [0.8588, 0.8863, 0.9255], "hex": "#dbe2ec"}},
      "160": {"$value": {"colorSpace": "srgb", "components": [0.8039, 0.8235, 0.8549], "hex": "#cdd2da"}},
      "300": {"$value": {"colorSpace": "srgb", "components": [0.6392, 0.6745, 0.7294], "hex": "#a3acba"}},
      "395": {"$value": {"colorSpace": "srgb", "components": [0.5412, 0.5765, 0.6392], "hex": "#8a93a3"}},
      "515": {"$value": {"colorSpace": "srgb", "components": [0.4196, 0.4549, 0.5176], "hex": "#6b7484"}},
      "650": {"$value": {"colorSpace": "srgb", "components": [0.2902, 0.3255, 0.3882], "hex": "#4a5363"}},
      "750": {"$value": {"colorSpace": "srgb", "components": [0.2039, 0.2353, 0.2902], "hex": "#343c4a"}},
      "825": {"$value": {"colorSpace": "srgb", "components": [0.1412, 0.1686, 0.2157], "hex": "#242b37"}},
      "855": {"$value": {"colorSpace": "srgb", "components": [0.1137, 0.1451, 0.1961], "hex": "#1d2532"}},
      "880": {"$value": {"colorSpace": "srgb", "components": [0.102, 0.1216, 0.1686], "hex": "#1a1f2b"}},
      "920": {"$value": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "hex": "#0f1729"}},
      "925": {"$value": {"colorSpace": "srgb", "components": [0.0706, 0.0863, 0.1255], "hex": "#121620"}},
      "960": {"$value": {"colorSpace": "srgb", "components": [0.0431, 0.0549, 0.0784], "hex": "#0b0e14"}}
    },
    "amber": {
      "220": {"$value": {"colorSpace": "srgb", "components": [1, 0.698, 0.1412], "hex": "#ffb224"}},
      "360": {"$value": {"colorSpace": "srgb", "components": [0.851, 0.5412, 0], "hex": "#d98a00"}, "$description": "Logo dot in light mode only. Fails 3:1 on white, so never used on the site."},
      "420": {"$value": {"colorSpace": "srgb", "components": [0.7608, 0.4863, 0], "hex": "#c27c00"}},
      "570": {"$value": {"colorSpace": "srgb", "components": [0.5608, 0.3529, 0], "hex": "#8f5a00"}}
    },
    "blue": {
      "325": {"$value": {"colorSpace": "srgb", "components": [0.4784, 0.6353, 1], "hex": "#7aa2ff"}},
      "575": {"$value": {"colorSpace": "srgb", "components": [0.1216, 0.3569, 0.8471], "hex": "#1f5bd8"}}
    },
    "green": {
      "290": {"$value": {"colorSpace": "srgb", "components": [0.298, 0.7647, 0.5412], "hex": "#4cc38a"}},
      "550": {"$value": {"colorSpace": "srgb", "components": [0.0549, 0.4784, 0.3333], "hex": "#0e7a55"}}
    },
    "red": {
      "360": {"$value": {"colorSpace": "srgb", "components": [1, 0.4196, 0.3529], "hex": "#ff6b5a"}},
      "545": {"$value": {"colorSpace": "srgb", "components": [0.7843, 0.2, 0.1216], "hex": "#c8331f"}}
    }
  },
  "font": {
    "family": {
      "$type": "fontFamily",
      "sans": {"$value": ["Instrument Sans", "ui-sans-serif", "system-ui", "sans-serif"]},
      "mono": {"$value": ["JetBrains Mono", "ui-monospace", "SFMono-Regular", "Menlo", "monospace"]}
    },
    "weight": {
      "$type": "fontWeight",
      "regular": {"$value": 400},
      "medium": {"$value": 500},
      "semibold": {"$value": 600}
    },
    "size": {
      "$type": "dimension",
      "12": {"$value": {"value": 12, "unit": "px"}},
      "13": {"$value": {"value": 13, "unit": "px"}},
      "14": {"$value": {"value": 14, "unit": "px"}},
      "16": {"$value": {"value": 16, "unit": "px"}},
      "18": {"$value": {"value": 18, "unit": "px"}},
      "20": {"$value": {"value": 20, "unit": "px"}},
      "24": {"$value": {"value": 24, "unit": "px"}},
      "30": {"$value": {"value": 30, "unit": "px"}},
      "38": {"$value": {"value": 38, "unit": "px"}},
      "48": {"$value": {"value": 48, "unit": "px"}},
      "60": {"$value": {"value": 60, "unit": "px"}}
    },
    "leading": {
      "$type": "number",
      "tight": {"$value": 1.1, "$description": "H1, display"},
      "snug": {"$value": 1.25, "$description": "H2"},
      "heading": {"$value": 1.3, "$description": "H3"},
      "ui": {"$value": 1.5, "$description": "UI text, captions"},
      "code": {"$value": 1.6},
      "prose": {"$value": 1.7, "$description": "Docs body text"}
    },
    "tracking": {
      "$type": "number",
      "$description": "Letter spacing in em. DTCG dimensions allow only px and rem, so these are plain numbers.",
      "display": {"$value": -0.035},
      "h1": {"$value": -0.03},
      "h2": {"$value": -0.02},
      "h3": {"$value": -0.01},
      "normal": {"$value": 0}
    }
  },
  "space": {
    "$type": "dimension",
    "0": {"$value": {"value": 0, "unit": "px"}},
    "4": {"$value": {"value": 4, "unit": "px"}},
    "8": {"$value": {"value": 8, "unit": "px"}},
    "12": {"$value": {"value": 12, "unit": "px"}},
    "16": {"$value": {"value": 16, "unit": "px"}},
    "20": {"$value": {"value": 20, "unit": "px"}},
    "24": {"$value": {"value": 24, "unit": "px"}},
    "32": {"$value": {"value": 32, "unit": "px"}},
    "40": {"$value": {"value": 40, "unit": "px"}},
    "48": {"$value": {"value": 48, "unit": "px"}},
    "64": {"$value": {"value": 64, "unit": "px"}},
    "80": {"$value": {"value": 80, "unit": "px"}},
    "96": {"$value": {"value": 96, "unit": "px"}}
  },
  "radius": {
    "$type": "dimension",
    "none": {"$value": {"value": 0, "unit": "px"}},
    "sm": {"$value": {"value": 4, "unit": "px"}, "$description": "Inline code, badges"},
    "md": {"$value": {"value": 6, "unit": "px"}, "$description": "Buttons, inputs, callouts"},
    "lg": {"$value": {"value": 8, "unit": "px"}, "$description": "Code blocks, cards"},
    "xl": {"$value": {"value": 12, "unit": "px"}, "$description": "Demo frame, dialogs"},
    "full": {"$value": {"value": 9999, "unit": "px"}, "$description": "Pills"}
  },
  "border": {
    "$type": "dimension",
    "width": {"$value": {"value": 1, "unit": "px"}}
  },
  "duration": {
    "$type": "duration",
    "fast": {"$value": {"value": 100, "unit": "ms"}},
    "base": {"$value": {"value": 160, "unit": "ms"}},
    "slow": {"$value": {"value": 240, "unit": "ms"}}
  },
  "easing": {
    "$type": "cubicBezier",
    "standard": {"$value": [0.2, 0, 0, 1]}
  },
  "layout": {
    "$type": "dimension",
    "header": {"$value": {"value": 64, "unit": "px"}},
    "sidebar": {"$value": {"value": 280, "unit": "px"}},
    "prose": {"$value": {"value": 640, "unit": "px"}},
    "toc": {"$value": {"value": 220, "unit": "px"}},
    "page": {"$value": {"value": 1400, "unit": "px"}},
    "landing": {"$value": {"value": 1200, "unit": "px"}},
    "gutter": {"$value": {"value": 16, "unit": "px"}, "$description": "Minimum side gutter on phones"}
  },
  "z": {
    "$type": "number",
    "base": {"$value": 0},
    "sticky": {"$value": 10},
    "sidebar": {"$value": 20},
    "header": {"$value": 30},
    "drawer": {"$value": 40},
    "popover": {"$value": 50},
    "skip-link": {"$value": 60}
  }
}

Notes on the format:

  • Letter spacing is in em, but a DTCG dimension allows only px and rem, so tracking is stored as number tokens with the unit in the group's $description.
  • {…} values are aliases. The resolver merges base and one theme file before resolving them, so a theme file may point at palette.* in the base file.

CSS custom properties

Light values sit on :root, dark values on .dark. Fumadocs UI's RootProvider wraps the site in next-themes with attribute: "class", defaultTheme: "system" and disableTransitionOnChange: true, so class="dark" lands on <html> when the OS or the toggle asks for dark (Fumadocs: Themes; fumadocs-ui 16.17.8, dist/provider/base.js). Fumadocs' preset.css defines Tailwind's dark: variant on the same class. Don't set --wc-* variables on inner elements. Keep suppressHydrationWarning on <html>, as next-themes requires.

/* website/app/global.css — part 1: tokens */
@import 'tailwindcss';
@import 'fumadocs-ui/css/preset.css';
/* No Fumadocs colour preset (neutral.css and friends): part 3 sets every --color-fd-* itself. */

:root {
  --wc-bg: #FFFFFF;
  --wc-surface: #F6F7F9;
  --wc-raised: #FFFFFF;
  --wc-rule: #E3E6EB;
  --wc-rule-strong: #CDD2DA;
  --wc-text: #0F1729;
  --wc-text-2: #4A5363;
  --wc-text-3: #6B7484;
  --wc-work: #DBE2EC;
  --wc-used: var(--wc-text);
  --wc-accent: #C27C00;
  --wc-accent-text: #8F5A00;
  --wc-focus: #1F5BD8;
  --wc-link: var(--wc-text);
  --wc-link-underline: var(--wc-rule-strong);
  --wc-link-underline-hover: var(--wc-accent);
  --wc-success: #0E7A55;
  --wc-danger: #C8331F;
  --wc-info: #1F5BD8;
  --wc-warning: var(--wc-accent);
  --wc-warning-text: var(--wc-accent-text);
  --wc-overlay: rgb(15 23 41 / 0.2);
  --wc-shadow-popover: 0 1px 2px rgb(15 23 41 / 0.06), 0 8px 24px rgb(15 23 41 / 0.08);

  --wc-duration-fast: 100ms;
  --wc-duration-base: 160ms;
  --wc-duration-slow: 240ms;
  --wc-ease: cubic-bezier(0.2, 0, 0, 1);

  --wc-header: 64px;
  --wc-sidebar: 280px;
  --wc-prose: 640px;
  --wc-toc: 220px;
  --wc-page: 1400px;
  --wc-landing: 1200px;
  --wc-gutter: 16px;
}

.dark {
  --wc-bg: #0B0E14;
  --wc-surface: #121620;
  --wc-raised: #1A1F2B;
  --wc-rule: #242B37;
  --wc-rule-strong: #343C4A;
  --wc-text: #E8ECF2;
  --wc-text-2: #A3ACBA;
  --wc-text-3: #8A93A3;
  --wc-work: #1D2532;
  --wc-accent: #FFB224;
  --wc-accent-text: #FFB224;
  --wc-focus: #7AA2FF;
  --wc-success: #4CC38A;
  --wc-danger: #FF6B5A;
  --wc-info: #7AA2FF;
  --wc-overlay: rgb(0 0 0 / 0.5);
  --wc-shadow-popover: 0 8px 24px rgb(0 0 0 / 0.5);
}

Aliases (--wc-used, --wc-link, --wc-warning …) are written once in :root and follow the dark values automatically.

Tailwind v4 @theme

Tailwind v4 makes utilities from theme variables in known namespaces: --color-* gives bg-*, text-*, border-*, fill-*; --text-* gives font sizes with optional --line-height, --letter-spacing and --font-weight companions; --font-*, --ease-*, --shadow-* likewise (Tailwind: Theme variables, font-size). Variables that point at other variables go in @theme inline, so the utility uses var(--wc-…) directly and follows .dark.

/* website/app/global.css — part 2: Tailwind theme */
@theme inline {
  --color-bg: var(--wc-bg);
  --color-surface: var(--wc-surface);
  --color-raised: var(--wc-raised);
  --color-rule: var(--wc-rule);
  --color-rule-strong: var(--wc-rule-strong);
  --color-text: var(--wc-text);
  --color-text-2: var(--wc-text-2);
  --color-text-3: var(--wc-text-3);
  --color-work: var(--wc-work);
  --color-used: var(--wc-used);
  --color-accent: var(--wc-accent);
  --color-accent-text: var(--wc-accent-text);
  --color-focus: var(--wc-focus);
  --color-success: var(--wc-success);
  --color-danger: var(--wc-danger);
  --color-info: var(--wc-info);
  --color-warning: var(--wc-warning);
  --color-warning-text: var(--wc-warning-text);

  --font-sans: var(--font-instrument-sans), ui-sans-serif, system-ui, sans-serif;
  --font-mono: var(--font-jetbrains-mono), ui-monospace, SFMono-Regular, Menlo, monospace;

  --shadow-popover: var(--wc-shadow-popover);
}

@theme {
  --text-caption: 0.75rem;
  --text-caption--line-height: 1.5;
  --text-code: 0.8125rem;
  --text-code--line-height: 1.6;
  --text-ui: 0.875rem;
  --text-ui--line-height: 1.5;
  --text-body: 1rem;
  --text-body--line-height: 1.7;
  --text-lead: 1.125rem;
  --text-lead--line-height: 1.6;
  --text-h3: 1.25rem;
  --text-h3--line-height: 1.3;
  --text-h3--letter-spacing: -0.01em;
  --text-h3--font-weight: 600;
  --text-h2: 1.5rem;
  --text-h2--line-height: 1.25;
  --text-h2--letter-spacing: -0.02em;
  --text-h2--font-weight: 600;
  --text-section: 1.875rem;
  --text-section--line-height: 1.25;
  --text-section--letter-spacing: -0.02em;
  --text-section--font-weight: 600;
  --text-h1: 2.375rem;
  --text-h1--line-height: 1.1;
  --text-h1--letter-spacing: -0.03em;
  --text-h1--font-weight: 600;
  --text-display-sm: 2.375rem;
  --text-display-sm--line-height: 1.1;
  --text-display-sm--letter-spacing: -0.035em;
  --text-display-sm--font-weight: 600;
  --text-display-md: 3rem;
  --text-display-md--line-height: 1.1;
  --text-display-md--letter-spacing: -0.035em;
  --text-display-md--font-weight: 600;
  --text-display: 3.75rem;
  --text-display--line-height: 1.1;
  --text-display--letter-spacing: -0.035em;
  --text-display--font-weight: 600;

  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --default-transition-duration: 160ms;
  --default-transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

Deliberately not in @theme:

  • Spacing, radius and breakpoints. Tailwind's defaults already match: --spacing: 0.25rem, --radius-sm … --radius-xl = 4, 6, 8, 12 px, breakpoints 640 to 1536 px (tailwindcss 4.3.3, theme.css).
  • Tailwind's colour palette and text-xs … text-6xl. Fumadocs UI's classes use them, so they stay defined. Our components use only the role utilities (text-body, not text-base; text-text-2, not text-gray-600).

Fumadocs UI mapping

Fumadocs UI colours every component through --color-fd-* variables (Fumadocs: Themes, colors). Since we import no colour preset, part 3 defines the full set that fumadocs-ui 16.17.8 declares in css/lib/default-colors.css. @theme static makes Tailwind emit them even when no utility uses them, which matters because Fumadocs also reads them directly (base styles, callout inline styles).

Most mappings are one to one (background → bg, border → rule, info → info …). The ones that are not obvious:

  • --color-fd-muted-foreground → text-2, not text-3, which fails 4.5:1 on surface in light mode. Fumadocs uses it for nav, TOC and code block titles.
  • --color-fd-card → surface. Fumadocs also paints the sidebar with it; part 4 sets the sidebar back to bg.
  • --color-fd-primary → text. Fumadocs uses it for primary buttons, active nav and TOC text and highlighted code lines; amber there would spread to buttons.
  • --color-fd-accent → rule. Despite the name it is a hover background, not our amber.
  • --color-fd-idea → info. Fumadocs' default is orange, which would read as amber; idea callouts are not used.
  • Diff backgrounds are success and danger at 12%.
/* website/app/global.css — part 3: Fumadocs UI colours */
@theme static {
  --color-fd-background: var(--wc-bg);
  --color-fd-foreground: var(--wc-text);
  --color-fd-muted: var(--wc-surface);
  --color-fd-muted-foreground: var(--wc-text-2);
  --color-fd-popover: var(--wc-raised);
  --color-fd-popover-foreground: var(--wc-text);
  --color-fd-card: var(--wc-surface);
  --color-fd-card-foreground: var(--wc-text);
  --color-fd-border: var(--wc-rule);
  --color-fd-primary: var(--wc-text);
  --color-fd-primary-foreground: var(--wc-bg);
  --color-fd-secondary: var(--wc-surface);
  --color-fd-secondary-foreground: var(--wc-text);
  --color-fd-accent: var(--wc-rule);
  --color-fd-accent-foreground: var(--wc-text);
  --color-fd-ring: var(--wc-focus);
  --color-fd-overlay: var(--wc-overlay);
  --color-fd-info: var(--wc-info);
  --color-fd-warning: var(--wc-warning);
  --color-fd-error: var(--wc-danger);
  --color-fd-success: var(--wc-success);
  --color-fd-idea: var(--wc-info);
  --color-fd-diff-add: color-mix(in srgb, var(--wc-success) 12%, transparent);
  --color-fd-diff-add-symbol: var(--wc-success);
  --color-fd-diff-remove: color-mix(in srgb, var(--wc-danger) 12%, transparent);
  --color-fd-diff-remove-symbol: var(--wc-danger);
}

Overrides where Fumadocs' defaults break our rules

Fumadocs marks the current page and the active tab with --color-fd-primary, which is ink for us. These rules add the amber markers, keep the sidebar on bg, and set the one focus style. They sit outside any @layer, so they win over Tailwind's @layer utilities regardless of specificity (MDN: @layer). The selectors were read from fumadocs-ui 16.17.8 (#nd-sidebar, data-active, Radix role="tab" with data-state); they are not a documented API, so check them after every Fumadocs upgrade.

/* website/app/global.css — part 4: overrides (un-layered) */

/* Sidebar sits on the page background, not on the card grey. */
#nd-sidebar {
  --color-fd-card: var(--wc-bg);
}

/* Current page: surface background, medium weight, 2px amber bar on the start edge. */
#nd-sidebar a[data-active='true'] {
  background-color: var(--wc-surface);
  color: var(--wc-text);
  font-weight: 500;
  box-shadow: inset 2px 0 0 var(--wc-accent);
  border-start-start-radius: 0;
  border-end-start-radius: 0;
}

/* Active tab: amber underline instead of ink. */
[role='tab'][data-state='active'] {
  border-bottom-color: var(--wc-accent);
}

/* Links: ink, underlined. */
.prose a {
  color: var(--wc-link);
  text-decoration-line: underline;
  text-decoration-color: var(--wc-link-underline);
  text-underline-offset: 4px;
}
.prose a:hover {
  text-decoration-color: var(--wc-link-underline-hover);
}

/* One focus style everywhere; switch off Fumadocs' ring so there is no double ring. */
:focus-visible {
  outline: 2px solid var(--wc-focus);
  outline-offset: 2px;
  --tw-ring-shadow: 0 0 #0000;
}

The current-page bar is an inset box-shadow so the text does not shift. It is a 2 px bar, not a shadow, so the no-shadows rule does not apply.

Using tokens in components

Do Don't
text-text-2, bg-surface, border-rule text-gray-600, bg-[#F6F7F9], border-neutral-200
border-text-3 on a text input border-rule-strong on a text input (below 3:1)
fill-accent on the due marker text-accent on words (use text-accent-text)
rounded-lg on a card rounded-[10px]
duration-160 ease-standard duration-300 ease-in-out
var(--wc-work) in an inline style or SVG #DBE2EC

After changing a token

  1. Run the site in light and dark mode; look at a docs page, the landing demo and the search dialog.
  2. Recompute any contrast ratio the change touches and update Color.
  3. If it is a brand colour, update the Canva files and exports listed on Brand.

Open items

  • Proposed: the website/tokens/ file paths and the check-tokens.mjs CI check.

Clone this wiki locally