Tokens.
This implementation reference maps the Foundations standards into named values for Figma and code. Use the names instead of raw values so modes and applications stay aligned.
- Pipeline
- Figma → CSS → Tailwind
- Modes
- Light · Dark
- Variables
- 90+ named
- Install
- @alix/tokens
One vocabulary
A token is a named decision. --primary is the name; #1b3644 is the value it happens to hold today. You type bg-primary, never bg-[#1b3644] — so the two navies below paint identically, but only one is a token. Name the role and a rebrand becomes one edit, dark mode follows for free, and a Figma variable maps straight to a class.
The pipeline
One token, end to end. A Figma variable becomes a value in semantic.css, a bridge in theme.css turns that value into a utility, you type the utility in JSX, and the pixel is painted. Two files do the work: semantic.css holds the values; theme.css is the @theme inline bridge — the half that actually makes the utility exist. Ship both, or components render unstyled.
A named decision by design.
The role's value, per mode.
This is what makes the utility exist.
What you actually type.
One value, on screen.
primary is decided once in Figma and never re-typed as a hex again. semantic.css holds the value; theme.css bridges it to a class.Light & dark
A token is one definition with two values. The .dark class remaps the variables underneath; the spelling never changes — bg-background is bg-background in both modes. So you never branch on theme in markup: the token layer owns the switch. For a dark panel inside a light page, reach for the Surface component (bgColorVariants) — it remaps the foreground tokens too, so text stays legible.
--border resolves to #e5e5e5 in light and #262626 in dark. You still type border-border — the value moves, the name holds.
Install
Three steps, once per project. Map the @alix registry in components.json, install the tokens item, then import it after Tailwind. Only then add components — the theme utilities they use don't exist until the bridge is present. Alix Sans loads separately, via @font-face at weight 400 (the house weight); it is not a token.
{
"registries": {
"@alix": "https://your-registry.example.com/r/{name}.json"
}
}npx shadcn@latest add @alix/tokens@import "tailwindcss";
@import "./tokens/index.css";npx shadcn@latest add @alix/button @alix/textAndMediaBlockColour
The complete colour surface, by group. Every row: the CSS variable, the Tailwind utility you type, the resolved light and dark values, and its role. Reach for the role — background, muted-foreground, chart-1 — never a raw hex.
| CSS variable | Tailwind | Light | Dark | Role | |
|---|---|---|---|---|---|
| --background | bg-background | #ffffff | #0a0a0a | Page surface | |
| --foreground | bg-foreground | #0a0a0a | #fafafa | Body text | |
| --card | bg-card | #ffffff | #171717 | Raised surface | |
| --muted | bg-muted | #f5f5f5 | #262626 | Subtle fill | |
| --muted-foreground | bg-muted-foreground | #525252 | #a3a3a3 | Secondary text | |
| --border | bg-border | #e5e5e5 | #262626 | Hairlines | |
| --accent | bg-accent | #f5f5f5 | #404040 | Hover / active fill | |
| --ring | bg-ring | #a3a3a3 | #737373 | Focus ring | |
| --card-foreground | bg-card-foreground | #0a0a0a | #fafafa | Text on card | |
| --accent-foreground | bg-accent-foreground | #171717 | #fafafa | Text on accent fill | |
| --popover | bg-popover | #ffffff | #262626 | Overlay surface (menus, tooltips) | |
| --popover-foreground | bg-popover-foreground | #0a0a0a | #fafafa | Text on popover | |
| --input | bg-input | #e5e5e5 | #262626 | Input border / control hairline |
| CSS variable | Tailwind | Light | Dark | Role | |
|---|---|---|---|---|---|
| --primary | bg-primary | #1b3644 | #1b3644 | Brand navy — anchor & the one dark surface | |
| --primary-foreground | bg-primary-foreground | #fafafa | #fafafa | On navy | |
| --secondary | bg-secondary | #eaa23a | #eaa23a | Gold — the single accent, never a surface | |
| --secondary-foreground | bg-secondary-foreground | #171717 | #171717 | On gold | |
| --sand | bg-sand | #c6b298 | #c6b298 | Warm neutral ground |
| CSS variable | Tailwind | Light | Dark | Role | |
|---|---|---|---|---|---|
| --destructive | bg-destructive | #dc2626 | #f87171 | Errors / destructive | |
| --success | bg-success | #16a34a | #22c55e | Success | |
| --warning | bg-warning | #d97706 | #f59e0b | Warning | |
| --destructive-foreground | bg-destructive-foreground | #ffffff | #0a0a0a | Text on destructive | |
| --success-foreground | bg-success-foreground | #0a0a0a | #0a0a0a | Text on success | |
| --warning-foreground | bg-warning-foreground | #0a0a0a | #0a0a0a | Text on warning |
| CSS variable | Tailwind | Light | Dark | Role | |
|---|---|---|---|---|---|
| --chart-1 | bg-chart-1 | #1b3644 | #7da6bb | Series 1 — navy, the protagonist | |
| --chart-2 | bg-chart-2 | #3f6b7d | #eaa23a | Series 2 — steel (a lighter rung of the navy ramp) | |
| --chart-3 | bg-chart-3 | #c6b298 | #c6b298 | Series 3 — sand | |
| --chart-4 | bg-chart-4 | #eaa23a | #5f8294 | Series 4 — gold, the single accent | |
| --chart-5 | bg-chart-5 | #9aa7b1 | #c2ccd3 | Series 5 — cool grey, a quiet 5th |
| CSS variable | Tailwind | Light | Dark | Role | |
|---|---|---|---|---|---|
| --sidebar | bg-sidebar | #fafafa | #171717 | Sidebar surface | |
| --sidebar-foreground | bg-sidebar-foreground | #0a0a0a | #fafafa | Sidebar text | |
| --sidebar-primary | bg-sidebar-primary | #171717 | #1d4ed8 | Active / selected item | |
| --sidebar-primary-foreground | bg-sidebar-primary-foreground | #fafafa | #fafafa | Text on active item | |
| --sidebar-accent | bg-sidebar-accent | #f5f5f5 | #262626 | Hover fill | |
| --sidebar-accent-foreground | bg-sidebar-accent-foreground | #171717 | #fafafa | Text on hover fill | |
| --sidebar-border | bg-sidebar-border | #e5e5e5 | #262626 | Sidebar hairline | |
| --sidebar-ring | bg-sidebar-ring | #a3a3a3 | #525252 | Sidebar focus ring |
Interaction states
Hover, active, and subtle fills are derived with color-mix — a percentage of the base colour blended over the surface, so a control's states read as designed on any ground instead of bleeding through an alpha. Because color-mix resolves at use time, each recomputes correctly under .dark.
| CSS variable | Tailwind | Recipe | Role | |
|---|---|---|---|---|
| --primary-hover | bg-primary-hover | 80% primary over background | Primary button — hover | |
| --primary-active | bg-primary-active | 60% primary over background | Primary button — pressed | |
| --secondary-hover | bg-secondary-hover | 80% secondary over background | Secondary (gold) — hover | |
| --secondary-active | bg-secondary-active | 60% secondary over background | Secondary (gold) — pressed | |
| --input-hover | bg-input-hover | 50% input over background | Input hairline — hover | |
| --input-active | bg-input-active | 30% input over background | Input hairline — active | |
| --destructive-subtle | bg-destructive-subtle | 10% destructive over background | Destructive tinted surface — rest | |
| --destructive-subtle-hover | bg-destructive-subtle-hover | 20% destructive over background | Destructive tinted surface — hover | |
| --destructive-subtle-active | bg-destructive-subtle-active | 30% destructive over background | Destructive tinted surface — pressed |
Gradients & focus
Two gradient grounds and the focus treatment. The gradients are custom @utility classes, not bg-* colours. The focus ring ships as one @utility so every control opts in with a single class — outline reset plus the visible ring, tokenised.
| CSS variable | Tailwind | Role | |
|---|---|---|---|
| --grey-gradient | bg-grey-gradient | Cool light surface — an alternative ground to white | |
| --gold-gradient | bg-gold-gradient | Deep gold surface (dark-surface text remap applies) |
.default-focus-ring
focus:outline-none focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50
Type
Alix Sans at weight 400 throughout — hierarchy comes from size and colour, never weight. Display headings are fixed utilities; the -r variants step down on mobile and back up at md (768px). The text scale is the reading scale. Two font stacks back it all.
| Utility | Size | Leading | Tracking | Specimen |
|---|---|---|---|---|
| heading-xl | 60px | 72px | 0 | Axis |
| heading-lg | 48px | 58px | -0.4px | Axis |
| heading-md | 36px | 43px | -0.2px | Axis |
| heading-sm | 24px | 32px | 0 | Axis |
| Utility | Mobile | ≥ md | Specimen |
|---|---|---|---|
| heading-xl-r | 36/42px | 60/72px | Axis |
| heading-lg-r | 30/38px | 48/58px | Axis |
| heading-md-r | 24/32px | 36/43px | Axis |
| Utility | Size | Leading | Specimen |
|---|---|---|---|
| text-xl | 20px | 28px | The quick brown fox |
| text-lg | 18px | 24px | The quick brown fox |
| text-base | 16px | 24px | The quick brown fox |
| text-sm | 14px | 20px | The quick brown fox |
| text-xs | 12px | 16px | The quick brown fox |
| CSS variable | Tailwind | Stack |
|---|---|---|
| --font-sans | font-sans | "Alix Sans", ui-sans-serif, system-ui, sans-serifThe product sans, loaded via @font-face (Alix Sans, weight 400/700). |
| --font-mono | font-mono | "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospaceData/spec/code — falls back to the platform mono so builds fetch no web font. |
Space
One gap vocabulary drives Stack, Inline, and Grid — each named step maps to a Tailwind gap-* class you type. Page layout is deliberately responsive utility classes, not variables (the gutter and rhythm grow with the viewport at the breakpoints below). The container scale backs both content width and block min-height.
| Step | Class you type | Value | Specimen |
|---|---|---|---|
| none | gap-0 | 0px | |
| xxs | gap-1 | 4px | |
| xs | gap-2 | 8px | |
| sm | gap-3 | 12px | |
| md | gap-4 | 16px | |
| lg | gap-6 | 24px | |
| xl | gap-10 | 40px |
| Name | Classes | Resolved | Note |
|---|---|---|---|
| Page gutter | px-6 md:px-10 lg:px-16 | 24 · 40 · 64px | Horizontal padding supplied by SiteContainer. Apply it to editorial content, not full-bleed sections. |
| Section rhythm | py-20 md:py-28 | 80 · 112px | Vertical padding set on each editorial section. |
| Tile inset | p-10 | 40px | Internal padding supplied by Surface-based tiles. Other blocks own their internal spacing. |
| Canvas max-width | mx-auto w-full max-w-page | 1728px | Apply once to the page root. The page fills the viewport up to 1728px, then centres. |
| Token | rem | px |
|---|---|---|
| --container-sm | 24rem | 384px |
| --container-md | 28rem | 448px |
| --container-lg | 32rem | 512px |
| --container-xl | 36rem | 576px |
| --container-2xl | 42rem | 672px |
| CSS variable | rem | px | Prefix | Role |
|---|---|---|---|---|
| --breakpoint-sm | 40rem | 640px | sm: | Use only for small-control adjustments. Page layout starts changing at md. |
| --breakpoint-md | 48rem | 768px | md: | Primary page breakpoint. Grids move to two columns; display type, page gutter and section padding step up. |
| --breakpoint-lg | 64rem | 1024px | lg: | Secondary page breakpoint. Card rows reach their full column count and the page gutter reaches 64px. |
| --breakpoint-xl | 80rem | 1280px | xl: | Available from Tailwind. Used only by a small number of components. |
| --breakpoint-2xl | 96rem | 1536px | 2xl: | Available from Tailwind. No current page layout depends on it. |
Figma layout names
These names live in the Figma Style Guide as designer vocabulary — but they do NOT ship as CSS variables or utilities. The layout behaviour is expressed as the responsive classes in 0.9 instead. Documented here so the shared language is honest, without pretending a flat value ships.
| Figma name | Figma px | Where the behaviour actually lives |
|---|---|---|
| container-padding-x | 24px | Figma stores a single 24px gutter. Code uses responsive px-6 md:px-10 lg:px-16 through SiteContainer. |
| section-padding-y | 96px | Figma stores 96px. Code uses py-20 md:py-28 for editorial sections; full-bleed blocks own their internal spacing. |
| section-title-gap-sm | 16px | SectionTitle uses gap-2 at base and gap-3 from lg for size sm. |
| section-title-gap-md | 20px | SectionTitle uses gap-3 at base and gap-4 from lg for size md. |
| section-title-gap-lg | 20px | SectionTitle uses gap-4 at base and gap-5 from lg for size lg. |
| section-title-gap-xl | 24px | SectionTitle uses gap-5 at base and gap-6 from lg for size xl. |
Shape & elevation
The radius scale derives from a single --radius: 0px knob; every step clamps to a sharp 0 corner (only xl lifts to 4px, and rounded-full is reserved for where round carries meaning). Elevation is the exception too — six shadow steps, most surfaces sit on a hairline instead. See the Shape and Elevation chapters for the full argument.
| Token | Tailwind | Derivation | px | Note |
|---|---|---|---|---|
| --radius | — | base knob = 0px | 0px | The single source; every step derives from it. |
| --radius-sm | rounded-sm | calc(--radius − 4px) = −4px → clamps to 0 | 0px | Sharp corner. |
| --radius-md | rounded-md | calc(--radius − 2px) = −2px → clamps to 0 | 0px | Sharp corner. |
| --radius-lg | rounded-lg | var(--radius) = 0 | 0px | Sharp corner. |
| --radius-xl | rounded-xl | calc(--radius + 4px) = 4px | 4px | Reserved headroom, not a brand surface. |
| --rounded-full | rounded-full | Tailwind built-in (9999px) | 9999px | The only active curve, used where round = meaning. |
| Tailwind | Value | |
|---|---|---|
| shadow-xs | 0 1px 2px rgba(0,0,0,.05) | |
| shadow-sm | 0 1px 3px rgba(0,0,0,.1) | |
| shadow-md | 0 4px 6px -1px rgba(0,0,0,.1) | |
| shadow-lg | 0 10px 15px -3px rgba(0,0,0,.1) | |
| shadow-xl | 0 20px 25px -5px rgba(0,0,0,.1) | |
| shadow-2xl | 0 25px 50px -12px rgba(0,0,0,.25) |
Misuse
The token layer only pays off if you go through it. Type the role, stay on the scale, and never let markup decide the theme.