Design tokens
Airframe ships --af-* custom properties. Override them in your stylesheet — no token build, no config file. The browser applies the change.
:root {
--af-base-primary: #018183;
}That is a primitive. Semantic colors and pattern chrome alias it, so one line retints the system.
Theming is the workflow (light, dark, high contrast, data-brand). Tokens full list is every name with defaults per mode.
Source of truth
Section titled “Source of truth”Commit CSS. JSON, TypeScript, and Figma are exports of the same --af-* names — not a second palette.
Canonical
CSS in git
Colour, space, and type. Override --af-* in :root or
[data-brand].
Generated
Same names and values
- JSON · TypeScript · catalog
- DTCG —
af theme export - Figma Variables —
af theme export
@airframeui/tokens is authored as CSS. The build emits custom properties (bundled in @airframeui/core), JSON, a catalog, and TypeScript.
Most projects override primitives in :root and stop. To share colours with designers: Figma plugin. To map an existing token file: Theme package.
Three layers
Section titled “Three layers”Tokens stack. Change the highest layer that matches the job.
| Layer | Names | What it is | Override when |
|---|---|---|---|
| Primitive | --af-base-*, --af-space-*, --af-text-*, --af-radius-sm…--xl, --af-bp-* | Raw values. Hex, px, rem. No UI meaning. | Rebrand everything that aliases this value |
| Semantic | --af-color-*, --af-radius, --af-type-h1-*, --af-font-body | Meaning in the product. Patterns read these. | “Primary”, “body text”, or “product radius” should mean something else |
| Component | --af-btn-*, --af-card-*, --af-input-*, --af-control-* | Knobs on one pattern. Alias semantic tokens. | Only buttons (or cards, or fields) should diverge |
Do not skip layers in your own CSS. Patterns should consume semantic names (--af-color-primary), not --af-base-primary. Your :root overrides can sit on any layer.
:root {
/* Primitive — the hex */
--af-base-primary: #018183;
/* Semantic — what UI means (defaults already alias the primitive) */
--af-color-action: var(--af-color-primary);
/* Component — one pattern */
--af-btn-radius: var(--af-pill-radius);
}Primitives
Section titled “Primitives”Override --af-base-* to rebrand. Neutrals and accents live here.
:root {
/* Accent colors */
--af-base-primary: var(--af-base-primary--light);
--af-base-secondary: var(--af-base-secondary--light);
--af-base-tertiary: var(--af-base-tertiary--light);
--af-base-success: var(--af-base-success--light);
--af-base-warning: var(--af-base-warning--light);
--af-base-danger: var(--af-base-danger--light);
--af-base-info: var(--af-base-info--light);
/* Neutrals */
--af-base-white: #ffffff;
--af-base-black: #000000;
--af-base-neutral-50: #f8fafc;
--af-base-neutral-100: #f1f5f9;
--af-base-neutral-200: #e2e8f0;
--af-base-neutral-300: #cbd5e1;
--af-base-neutral-400: #94a3b8;
--af-base-neutral-500: #64748b;
--af-base-neutral-600: #475569;
--af-base-neutral-700: #1d2434;
--af-base-neutral-800: #121620;
--af-base-neutral-900: #0d1117;
}Also primitive: space (--af-space-0…--8), type scale (--af-text-sm…--5xl), radius steps (--af-radius-sm…--full), breakpoints (--af-bp-md, …), shadows, z-index, motion.
Fonts:
:root {
--af-font-primary: 'Inter', system-ui, sans-serif;
--af-font-heading: 'Playfair Display', serif;
--af-font-body: var(--af-font-primary);
--af-font-code: var(--af-base-font-mono);
}Responsive suffixes (@md, @lg) read --af-bp-*. Defaults: xs 360px, sm 640px, md 768px, lg 1024px, xl 1280px, 2xl 1536px.
Semantic
Section titled “Semantic”--af-color-* is what patterns paint with: background, surface, text, border, primary, success, danger, and the rest. Override a semantic token when the role should change without retinting every primitive.
--af-color-action defaults to --af-color-primary. Override it when the CTA should differ from brand primary. Hover, active, soft, and hard variants follow via color-mix().
:root {
--af-color-action: #0d9488;
--af-color-on-action: #ffffff;
}Per-scheme suffixes (--light, --dark, --hc-light, --hc-dark) are in Theming.
Global color mix knobs
Section titled “Global color mix knobs”Hover, active, soft, hard, and border tints are color-mix() from these knobs:
:root {
--af-hover-mix: 12%;
--af-active-mix: 18%;
--af-soft-mix: 12%;
--af-hard-mix: 45%;
--af-hard-toward: var(--af-color-text);
--af-border-mix: 22%;
}Pair *-soft surfaces with *-hard text so small type meets WCAG AA. Do not use mid-tone --af-color-success (and the other intents) as small text on a soft tint. --af-hard-toward follows --af-color-text, so a dark-first --light brand keeps hard text light.
--af-radius is the product default. Pattern chrome goes through three families that alias it: --af-btn-radius, --af-input-radius, --af-card-radius. One :root --af-radius still retouches those families. --af-radius-sm (chips, badges, tooltips) does not follow --af-radius. Pills use --af-pill-radius.
Type roles (--af-type-h1-size, --af-type-body-size, …) alias the type scale. Override the role when h1 should grow without changing --af-text-3xl everywhere.
--af-duration-* is for transitions. --af-animation-duration-* is for keyframes. prefers-reduced-motion: reduce sets both to 0.01ms, including values you override. Full guide: Motion.
--af-hit-size is the minimum touch target (inputs use it for --af-input-min-height).
Layout tokens
Section titled “Layout tokens”--af-sidebar-width is in-flow chrome (af-app, af-sidebar). --af-sidebar-collapsed-width is the icon rail when af-app is af-is-collapsed (from lg up). --af-drawer-width is overlay panels (af-drawer). They are independent.
:root {
--af-layout-gap: var(--af-space-4);
--af-sidebar-width: 16rem;
--af-sidebar-collapsed-width: 3.5rem;
--af-drawer-width: 20rem;
--af-container-padding-x: var(--af-space-4);
--af-container-max-w: 90em;
}Also xs 25rem, sm 40rem, md 48rem, lg 64rem, xl 80rem, 2xl 96rem on --af-container-max-w-*.
Component
Section titled “Component”Thin knobs on top of semantic tokens. Override these instead of rewriting pattern CSS.
:root {
--af-btn-radius: var(--af-radius);
--af-btn-padding-x: var(--af-space-4);
--af-btn-border-width: var(--af-border-width);
--af-btn-border: transparent;
--af-card-radius: var(--af-radius);
--af-card-padding: var(--af-space-5);
--af-card-shadow: var(--af-shadow-sm);
--af-control-radius: var(--af-radius);
--af-control-border: var(--af-color-border);
--af-input-min-height: var(--af-hit-size);
--af-input-border-focus: var(--af-color-primary);
--af-link-primary: var(--af-color-primary);
--af-link-decoration: underline;
}Filled button hover/active use --af-btn-hover-brightness / --af-btn-active-brightness. Filled borders are off; set --af-btn-border to a color, or var(--af-btn-border-auto), to paint them. Outline uses --af-btn-outline-*. Ghost and link ignore that master.
a and .af-link use --af-link-*. Patterns that are themselves an <a> (af-btn, af-card, af-pill) opt out of link decoration.
Shared control tokens
Section titled “Shared control tokens”Inputs, selects, and textareas share --af-control-*. Checkboxes and radios use --af-choice-size. Switches use --af-switch-height (defaults to choice size) and --af-switch-width-ratio. Ranges use --af-range-track-height and --af-range-thumb-size.
Focus:
:root {
--af-focus-ring-color: var(--af-color-focus);
--af-focus-ring-width: 2px;
--af-focus-ring-offset: 2px;
}Every other --af-* name: Tokens full list.
Next steps
Section titled “Next steps”- Theming — light, dark, high contrast, and
data-brand - Tokens full list — every token, defaults per mode
- Figma plugin — sync CSS into Figma Variables
- Theme package — map, lint, and export DTCG
- Airframe → Figma — product overview