Theming
Airframe is customized with CSS variables. Override --af-* tokens in your stylesheet — no build step, no config file.
Four color schemes ship built-in: light, dark, high-contrast-light, and high-contrast-dark. Dark follows prefers-color-scheme; high contrast follows prefers-contrast.
Force light or dark with data-theme="light" / data-theme="dark" (or .dark) — the same attribute Starlight, next-themes, and most app shells already use.
Named brands are a separate axis: data-brand holds a palette. It can sit on <html> or any subtree, and it composes with light / dark / high contrast.
Full token lists live in Tokens and Tokens full list. This page is the workflow: brand once, then tune light / dark / high contrast.
Start here
Section titled “Start here”Override primitives in :root for a global brand change. Everything that depends on those tokens updates:
:root {
--af-base-primary: #0876dd;
--af-base-secondary: #6b7280;
--af-base-success: #10b981;
--af-base-danger: #ef4444;
--af-base-warning: #f59e0b;
--af-base-info: #3b82f6;
--af-font-body: 'Inter', system-ui, sans-serif;
--af-radius: 0.5rem;
}When to use: One palette, same hex in every scheme — the usual first step.
Limitation: An unsuffixed token is the resolved color. Setting --af-base-primary: #c23018 pins that hex across light and dark. For different colors per scheme, or more than one brand, use suffixes and data-brand (below).
Color scheme vs brand
Section titled “Color scheme vs brand”Two independent axes. Components never see either attribute — they read unsuffixed tokens like --af-color-primary.
| Attribute | Meaning | Values |
|---|---|---|
data-theme | Mode (light / dark) | light, dark only. Omit to follow the OS. Same contract as Starlight / next-themes. |
data-brand | Named palette | Any name you choose (delta, vortex, …). Inherits to the subtree. |
High contrast is not a data-theme value. It stays on prefers-contrast.
<html data-theme="dark" data-brand="delta">
<body>
<main>Delta, dark mode</main>
<aside data-brand="vortex">Vortex, still dark</aside>
</body>
</html>data-theme accepts only light and dark. Do not put brand names or hc-light / hc-dark on it — that collides with the libraries above and with Airframe’s scheme switch.
Multiple brands
Section titled “Multiple brands”Each brand is a CSS block that sets stored suffixes (--light, --dark, --hc-light, --hc-dark). Airframe points the unsuffixed token at the active scheme.
[data-brand='delta'] {
--af-base-primary--light: #c23018;
--af-base-primary--dark: #e85d2a;
--af-base-primary--hc-light: #9a2a10;
--af-base-primary--hc-dark: #f08a5c;
--af-color-background--light: #fefaf8;
--af-color-background--dark: #1a0b08;
--af-color-surface-secondary--light: #f9ece7;
--af-color-surface-secondary--dark: #2a1410;
--af-font-heading: 'Delta Sans', system-ui, sans-serif;
}
[data-brand='vortex'] {
--af-base-primary--light: #00d2cc;
--af-base-primary--dark: #00e0d4;
--af-color-on-primary--light: #042628;
--af-color-on-primary--dark: #042628;
--af-color-background--light: #061516;
--af-color-background--dark: #030a0b;
--af-color-text--light: #e8fafa;
--af-color-text--dark: #e8fafa;
--af-color-surface-tertiary--light: #010101;
--af-color-surface-tertiary--dark: #010101;
}Omit any suffix you do not need. --dark falls back to --light for intent primitives. Surfaces have built-in dark defaults if you skip --af-color-background--dark.
What to put on a brand
Section titled “What to put on a brand”Put on [data-brand] | Leave alone |
|---|---|
--af-base-primary--light / --dark / --hc-* (intents) | Unsuffixed --af-base-primary, --af-color-* |
--af-color-background--light / --dark (surfaces, text) | --af-color-primary-hover and other color-mix |
| Fonts, radius, control height if they differ per brand | Breakpoints, z-index, motion unless you must |
Switch in JavaScript
Section titled “Switch in JavaScript”document.documentElement.setAttribute('data-brand', 'delta');
document.documentElement.setAttribute('data-theme', 'dark');
// Scoped: only this subtree changes palette
panel.setAttribute('data-brand', 'vortex');Components do not care
Section titled “Components do not care”Patterns use --af-color-primary, --af-color-background, --af-color-text. They do not read --light, data-theme, or data-brand. When the attributes change, those resolved variables update and the UI follows.
Why --light exists
Section titled “Why --light exists”Each color has stored scheme values and one resolved token that components actually use:
- Stored:
--af-base-primary--light,--af-base-primary--dark,--af-base-primary--hc-light,--af-base-primary--hc-dark - Resolved:
--af-base-primary— Airframe points this at the active scheme
Light mode reads --light. Dark mode reassigns the resolved token to --dark. High contrast reassigns it to --hc-light or --hc-dark. All four stored values can exist at once on [data-brand]; only the active scheme is applied.
If you set the resolved token yourself:
[data-brand='delta'] {
--af-base-primary: #c23018; /* resolved — pins this hex in every scheme */
--af-base-primary--dark: #e85d2a; /* stored, but never read */
}--af-base-primary: #c23018 replaces Airframe’s var(--af-base-primary--light) pointer. Dark mode has nothing left to reassign, so --dark is ignored. --af-base-primary--light is the light input; leave the unsuffixed name to Airframe.
Same rule for semantic colors: set --af-color-background--light and --af-color-background--dark, not --af-color-background.
Unsuffixed --af-base-primary in :root is still valid when you want one hex everywhere.
Force a scheme
Section titled “Force a scheme”Dark mode follows prefers-color-scheme automatically after you import the core CSS. To override the system:
<!-- Force dark -->
<html data-theme="dark">
<body>...</body>
</html>
<!-- Force light -->
<html data-theme="light">
<body>...</body>
</html>
<!-- Also supported -->
<html class="dark">
<body>...</body>
</html>data-theme is the brand’s light or dark scheme. Nested data-theme="light" on a panel still reads --af-color-text--light. If that brand stores a dark page in --light, the panel stays dark.
Lock a canvas
Section titled “Lock a canvas”.af-force-light / .af-force-dark lock a subtree to the catalog light or dark canvas (surfaces, text, borders). Intents (--af-color-primary, --af-color-on-primary) still follow the live brand.
Use this for chrome that must stay light-on-white or dark-on-near-black regardless of scheme or a dark-first --light palette: a Figma-style panel, a terminal card, a marketing band.
<figure class="af-force-light">
<button class="af-btn af-is-secondary af-is-outline">Always dark ink</button>
</figure>
<section class="af-force-dark">
<input class="af-input" placeholder="Always light ink" />
</section>Color tokens
Section titled “Color tokens”Primitives vs semantic
Section titled “Primitives vs semantic”| Layer | Prefix | Use for |
|---|---|---|
| Primitives | --af-base-* | Simple global brand swaps |
| Semantic | --af-color-* | Surfaces, text, borders, per-scheme control |
Per-scheme overrides
Section titled “Per-scheme overrides”Override stored tokens with suffixes --light, --dark, --hc-light, or --hc-dark. All intent primitives (primary, secondary, tertiary, success, warning, danger, info) support the same split. If you omit --dark, dark mode keeps the --light hex.
:root {
--af-base-success--light: #16a34a;
--af-base-success--dark: #4ade80;
--af-base-danger--hc-light: #b91c1c;
--af-color-action--hc-light: #0066cc;
}When to use: Different accents per scheme, stronger colors for high contrast, fine-grained surfaces and text.
Interaction states
Section titled “Interaction states”Hover, active, soft, and hard variants are derived via color-mix():
:root {
--af-hover-mix: 12%;
--af-active-mix: 18%;
--af-soft-mix: 12%;
--af-hard-mix: 45%; /* AA text on soft intent surfaces */
}Badges, chips, and KPI deltas use *-hard text on *-soft backgrounds. Keep --af-hard-mix high enough that success/warning/info hard text clears 4.5:1 on soft.
Dark mode
Section titled “Dark mode”Default behavior
Section titled “Default behavior”When you import the core CSS, dark mode follows prefers-color-scheme. No extra setup.
Custom dark colors
Section titled “Custom dark colors”Override dark colors with the --dark suffix. This works whether dark mode comes from the media query, data-theme, or .dark:
:root {
--af-color-background--dark: #0a0a0a;
--af-color-surface-primary--dark: #0a0a0a;
--af-color-text--dark: #ffffff;
--af-color-border--dark: #333333;
--af-base-primary--dark: #38bdf8;
}Pair a custom light surface with --af-color-background--light. You can still override --af-base-primary for a global change; --light / --dark suffixes give scheme-specific control.
High contrast
Section titled “High contrast”High-contrast schemes apply automatically when the OS enables high contrast (prefers-contrast: more or high). If dark mode and high contrast are both on, high-contrast dark tokens apply.
Override with --hc-light and --hc-dark on stored tokens (or primitives like --af-base-primary--hc-light):
:root {
--af-color-background--hc-light: #ffffff;
--af-color-surface-primary--hc-light: #ffffff;
--af-color-text--hc-light: #000000;
--af-color-text-muted--hc-light: #333333;
--af-color-border--hc-light: #000000;
--af-color-action--hc-light: #0066cc;
--af-color-on-action--hc-light: #ffffff;
--af-color-background--hc-dark: #000000;
--af-color-surface-primary--hc-dark: #1a1a1a;
--af-color-text--hc-dark: #ffffff;
--af-color-text-muted--hc-dark: #cccccc;
--af-color-border--hc-dark: #ffffff;
--af-color-action--hc-dark: #60a5fa;
--af-color-on-action--hc-dark: #000000;
}Always verify contrast against WCAG 2.2 AA (4.5:1 normal text, 3:1 large text / UI). Use the contrast checker before shipping.
Common high-contrast override tokens
Section titled “Common high-contrast override tokens”| Token | HC Light | HC Dark |
|---|---|---|
| background | --af-color-background--hc-light | --af-color-background--hc-dark |
| surface-primary | --af-color-surface-primary--hc-light | --af-color-surface-primary--hc-dark |
| text | --af-color-text--hc-light | --af-color-text--hc-dark |
| text-muted | --af-color-text-muted--hc-light | --af-color-text-muted--hc-dark |
| border | --af-color-border--hc-light | --af-color-border--hc-dark |
| action | --af-color-action--hc-light | --af-color-action--hc-dark |
| on-action | --af-color-on-action--hc-light | --af-color-on-action--hc-dark |
Testing high contrast
Section titled “Testing high contrast”Enable high contrast in the OS, or simulate prefers-contrast: more in DevTools. Check light and dark.
Typography and components
Section titled “Typography and components”Same mechanism — override tokens in :root or [data-brand]. Examples:
:root {
--af-font-primary: 'Inter', system-ui, sans-serif;
--af-font-heading: 'Inter', system-ui, sans-serif;
--af-control-min-height: 2.75rem;
--af-btn-radius: var(--af-pill-radius); /* optional: capsule CTAs */
--af-input-radius: var(--af-radius-sm); /* optional: tighter fields */
--af-card-radius: var(--af-radius-lg); /* boxed UI + overlays */
--af-duration-fast: 120ms;
}See Typography for type roles and Tokens for the full catalog (spacing, radius, shadows, motion, component tokens).
Complete example
Section titled “Complete example”:root {
--af-hover-mix: 12%;
--af-hard-mix: 45%;
--af-font-heading: ui-sans-serif, system-ui, sans-serif;
--af-control-min-height: 2.5rem;
--af-radius: 8px;
}
[data-brand='delta'] {
--af-base-primary--light: #c23018;
--af-base-primary--dark: #e85d2a;
--af-color-background--light: #fefaf8;
--af-color-background--dark: #1a0b08;
--af-color-surface-secondary--light: #f9ece7;
--af-color-surface-secondary--dark: #2a1410;
}One token source
Section titled “One token source”If you already have --color-* (or --bs-*, --mdc-*), map once into --af-*. Then stop duplicating semantics in two places.
For a product-owned namespace (gradients, elevation, display type) that should export to Figma as --demo-*, use BYO Tokens (theme.byoTokens.prefix) and optionally bridge into --af-*.
:root {
--af-base-primary: var(--color-primary);
/* After the spike, invert: --color-* aliases --af-* until old SCSS dies */
}Pick one canonical set for semantic colours Airframe patterns consume. Two parallel palettes forever is how a dual look leaks into every template. BYO tokens beside --af-* are fine when they are inventory or bridge cleanly. See Use as much as you need.
Extra tokens (charts, app-only)
Section titled “Extra tokens (charts, app-only)”Extra --af-* in theme.files export with af theme export (DTCG + plugin payload). Lint warns (extension-token); it does not fail. Stay in the --af-* namespace. Derived *-hover / *-active / *-soft / *-hard stay unmapped.
Unsuffixed hex overlays light only. Add --dark (and --hc-* if needed) for the other Figma modes. Prefer plain hex, px, or rem for extra tokens you invent. Catalog aliases already export as links between tokens.
:root {
--af-chart-1: #4f46e5;
--af-chart-1--dark: #a5b4fc;
}Airframe → Figma
Section titled “Airframe → Figma”Brand in CSS on this page, then export the same palette into Figma Variables. The project stays the source of truth.
Step-by-step: Figma plugin. Product overview: Airframe → Figma. Export layout and CLI flags: Theme package.
Next steps
Section titled “Next steps”- BYO Tokens — product-owned tokens alongside Airframe
- Tokens — how the token system works
- Tokens full list — every token, defaults per mode
- Figma plugin — plugin how-to and drift check
- Theme package — export commands and CLI detail
- Airframe → Figma — product overview