apps/shade/src/docs/tokens.mdx
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Tokens" /> <div className="sb-doc">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.
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.
Shade uses a small semantic palette layered on top of a larger raw palette.
Color tokens are grouped by what they're for, not by their RGB value. The current families are:
surface-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".text-primary, text-secondary, text-tertiary, text-inverse. Use these instead of the foreground variants when you want explicit hierarchy.border-subtle, border-default, border-strong, focus-ring. Three weights of border plus the focus ring color.state-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.
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.
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.
Three faces are available via --font-sans (Inter, the default UI face), --font-serif (Georgia, for editorial content), and --font-mono (Consolas, for code).
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.
--leading-base (1.5em) is the default. --leading-tight, --leading-tighter, --leading-supertight exist for headings and dense type.
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.
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.
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.
--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.
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).
Most of the time, you consume tokens through Tailwind utility classes:
// 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:
.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 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.
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.