Skip to content

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.

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].

tokens.base.css + your stylesheets · light · dark · high contrast

Generated

Same names and values

  • JSON · TypeScript · catalog
  • DTCG — af theme export
  • Figma Variables — af theme export
Figma reads the repo. It does not define the palette.

@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.

Tokens stack. Change the highest layer that matches the job.

LayerNamesWhat it isOverride 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-bodyMeaning 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);
}

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.

--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.

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).

--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-*.

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.

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.