Back to Ghost

Design tokens

apps/shade/src/docs/tokens.mdx

6.37.17.4 KB
Original Source

import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Tokens" /> <div className="sb-doc">

Design tokens

<p className="excerpt">Tokens are the smallest unit of the design system — a color, a size, a duration. Every other layer (primitives, components, features) ultimately resolves down to tokens. This page explains how they're defined, which families exist, and how to consume them.</p>

What a token is

In Shade, a token is a CSS custom property exposed through Tailwind v4's @theme block. So bg-background, text-foreground, and rounded-md are all backed by tokens that flip automatically when dark mode is on.

There are two kinds of token in practice:

Raw tokens are concrete values — --color-gray-500, --text-base, --spacing. They never change between modes; they just exist.

Semantic tokens are named by purpose, not value. --background, --foreground, --border-default, --surface-elevated. They reference raw tokens (or specific values) and they're allowed to flip between light and dark mode. You should almost always reach for the semantic token, not the raw one. That way the same component renders correctly in both modes without any work from you.

Where they're defined

Two files do the heavy lifting:

theme-variables.css holds the runtime values for every semantic token, including the dark-mode overrides. This is where --background becomes hsl(0 0% 100%) in light and hsl(216 11% 9%) in dark.

tailwind.theme.css is the Tailwind @theme block. It exposes both the raw token catalogue (the gray scale, the brand colors, the type ramp, the spacing unit) and the bindings that make bg-background resolve to var(--background).

If you want to know what a Tailwind class is actually doing, search tailwind.theme.css first.

Color

Shade uses a small semantic palette layered on top of a larger raw palette.

Semantic color families

Color tokens are grouped by what they're for, not by their RGB value. The current families are:

  • Surfacesurface-page, surface-panel, surface-elevated, surface-overlay, surface-inverse. Backgrounds with intent: "the page itself", "a card sitting on the page", "a popover floating above everything".
  • Texttext-primary, text-secondary, text-tertiary, text-inverse. Use these instead of the foreground variants when you want explicit hierarchy.
  • Border / focusborder-subtle, border-default, border-strong, focus-ring. Three weights of border plus the focus ring color.
  • Statestate-info, state-success, state-warning, state-danger. Plus matching -foreground variants for text on those backgrounds.

Every one of these flips between light and dark mode. You don't have to do anything for that to work.

Raw color palette

If you genuinely need a specific shade rather than a semantic token, the raw palette is also available — gray-50 through gray-900, the brand greens, plus chart colors (--chart-1 through --chart-5 and named accents like --chart-purple, --chart-rose).

A note on spelling: both grey and gray work as token aliases for legacy reasons, but gray is the convention going forward — it matches Tailwind.

Picking the right color

The default move is the semantic token: bg-background, text-foreground, border-border-default. Reach for raw colors when the semantic vocabulary doesn't fit (chart series, brand logos, illustrations).

What you should not do is hardcode hex or hsl() values, even temporarily. They don't theme, they don't dark-mode, and they don't show up in design audits.

Typography

Font families

Three faces are available via --font-sans (Inter, the default UI face), --font-serif (Georgia, for editorial content), and --font-mono (Consolas, for code).

Font sizes

The type ramp runs from --text-2xs (1.0rem) up to --text-9xl (12.8rem). Use the semantic text-base for body copy. Larger sizes have line-height variables baked in (--text-9xl--line-height: 1) so headlines don't get awkward leading.

Line height

--leading-base (1.5em) is the default. --leading-tight, --leading-tighter, --leading-supertight exist for headings and dense type.

Spacing

Shade's spacing scale is built on a 4px base (--spacing: 0.4rem). Tailwind utilities like p-4, gap-2, mt-3 derive from that base — p-4 is 4 × 0.4rem = 1.6rem.

For primitives (Stack, Inline, Box), use the semantic spacing scale instead of the raw Tailwind numbers: gap="md" rather than gap-4. The named values map onto the same underlying base unit but make intent readable: "a medium gap" instead of "the number four".

The semantic scale: none | xs | sm | md | lg | xl | 2xl.

Sizing

A few size tokens exist for shared dimensions:

  • --control-height (currently 34px) — the height of a medium form control. Used by Input, Select trigger, the "medium" Button size, etc.

If you need a control to match the others vertically, reach for this token rather than re-deriving it.

Radius

Border radius has both a numeric scale (--radius, --radius-sm, --radius-md, --radius-lg) and semantic aliases (radius-control for form controls, radius-surface for cards, radius-badge for pills, radius-pill for fully rounded).

Prefer the semantic version when it fits — it'll keep visual rhythm consistent if we ever shift the radius vocabulary.

Motion

--duration-fast, --duration-base, --duration-slow for timing. --ease-standard and --ease-emphasized for curves. There's also a set of pre-built named animations (--animate-fade-in, --animate-toaster-in, etc.) for common interactions.

Shadows and breakpoints

Standard shadows live on --shadow, --shadow-xs, --shadow-sm, --shadow-md, --shadow-lg. Breakpoints are defined as --breakpoint-sm through --breakpoint-xxxl, plus a few specials (--breakpoint-sidebar, --breakpoint-sidebarlg, --breakpoint-tablet).

How to consume tokens

Most of the time, you consume tokens through Tailwind utility classes:

tsx
// Good — semantic, themed
<div className="bg-background text-foreground border-border-default rounded-md">
    Hello
</div>

// Bad — hardcoded, breaks in dark mode
<div style={{background: '#fff', color: '#000', border: '1px solid #eee'}}>
    Hello
</div>

Inside a stylesheet (rare in Shade, but it happens), use the CSS variables directly:

css
.something {
    background: var(--surface-elevated);
    color: var(--surface-elevated-foreground);
}

Don't double-wrap variables in hsl() — they already contain hsl(...). Writing hsl(var(--background)) produces nonsense.

Dark mode

Dark mode is handled by toggling a .dark class on a Shade-scoped ancestor (ShadeApp does this for you). The CSS custom variant in theme-variables.css flips every semantic token under that class, so bg-background, text-foreground, etc. all switch automatically.

You don't write dark: variants in component code. The tokens do that work.

The exception is when a component genuinely needs a different layout or asset between modes (a logo, a custom illustration). For those rare cases, dark: Tailwind variants are fine. But for color and surface decisions, stick to semantic tokens and let them flip.

Reset

Shade ships its own scoped CSS reset (preflight.css) so it doesn't fight whatever reset the rest of the app might use. You don't need to do anything to get it — it's imported by styles.css.

</div>