Design tokens
Everything visual in forte-ui resolves through a CSS custom property. Components never contain a hardcoded color, size or duration — which is why re-pointing a token restyles every component at once, coherently. This page is the global token reference; the tables are generated from the shipped stylesheets at build time, so they cannot drift from what actually renders. Per-component knobs (--forte-button-*, --forte-dialog-*, …) are a separate mechanism documented on Styling components and on each component's own page.
How to override a token
Set it on :root, or on a theme scope to affect one subtree:
:root {
--forte-radius-control: var(--forte-radius-2);
--forte-shadow-2: 0 2px 8px oklch(0 0 0 / 0.12);
}Your CSS always wins. The library ships every rule inside @layer forte.*, and layered rules lose to unlayered author CSS regardless of specificity — no !important needed, ever.
Two things to know before overriding:
- A typo fails silently.
var(--forte-color-pannel)is not an error — the declaration simply becomes invalid at computed-value time and the element inherits instead. Check names against these tables. - Preset-owned tokens live on
:rootalone. Tokens marked rewritten by presets below are whatdata-forte-radiusanddata-forte-densityrewrite. Overriding one on:rootmoves the default, but any preset attribute on an ancestor still wins for its subtree.
Color
The accent, secondary and gray ramps — --forte-accent-1…12, --forte-secondary-1…12, --forte-gray-1…12 — are derived from the seeds described on the Theming page, in both light and dark mode. Prefer changing the seed over overriding ramp steps: the derivation is what keeps the palette coherent and the contrast pairs certified.
Components consume the ramps through semantic aliases, which are the right level for surgical overrides — they re-derive per scope and follow light/dark automatically when you assign them a ramp step:
Surfaces, text and borders
| Token | Default | Notes |
|---|---|---|
--forte-color-background | var(--forte-gray-1) | |
--forte-color-border | color-mix(in oklab, var(--forte-gray-12) 12%, transparent) | retuned under prefers-contrast: more |
--forte-color-border-muted | color-mix(in oklab, var(--forte-gray-12) 8%, transparent) | retuned under prefers-contrast: more |
--forte-color-border-strong | color-mix(in oklab, var(--forte-gray-12) 20%, transparent) | retuned under prefers-contrast: more |
--forte-color-foreground | var(--forte-gray-12) | |
--forte-color-foreground-muted | var(--forte-gray-11) | retuned under prefers-contrast: more |
--forte-color-foreground-subtle | var(--forte-gray-10) | retuned under prefers-contrast: more |
--forte-color-overlay | var(--forte-gray-1) | |
--forte-color-panel | var(--forte-gray-2) | |
--forte-color-panel-active | var(--forte-gray-4) | |
--forte-color-panel-hover | var(--forte-gray-3) |
Brand
| Token | Default |
|---|---|
--forte-color-on-primary | oklch(0.995 0 0) |
--forte-color-on-secondary | oklch(0.995 0 0) |
--forte-color-primary | var(--forte-accent-9) |
--forte-color-primary-active | var(--forte-accent-11) |
--forte-color-primary-border | var(--forte-accent-7) |
--forte-color-primary-hover | var(--forte-accent-10) |
--forte-color-primary-soft | var(--forte-accent-3) |
--forte-color-primary-soft-active | var(--forte-accent-5) |
--forte-color-primary-soft-hover | var(--forte-accent-4) |
--forte-color-primary-text | var(--forte-accent-11) |
--forte-color-secondary | var(--forte-secondary-9) |
--forte-color-secondary-active | var(--forte-secondary-11) |
--forte-color-secondary-border | var(--forte-secondary-7) |
--forte-color-secondary-hover | var(--forte-secondary-10) |
--forte-color-secondary-soft | var(--forte-secondary-3) |
--forte-color-secondary-soft-active | var(--forte-secondary-5) |
--forte-color-secondary-soft-hover | var(--forte-secondary-4) |
--forte-color-secondary-text | var(--forte-secondary-11) |
Status
Status hues are fixed rather than seed-derived — a brand color must not be able to make an error look reassuring. The aliases below are what components read; behind them sit fixed ramp steps (--forte-danger-2…11, --forte-success-*, --forte-warning-*, --forte-info-*) you can retune if your palette needs different alarm colors.
| Token | Default |
|---|---|
--forte-color-danger | var(--forte-danger-9) |
--forte-color-danger-border | var(--forte-danger-7) |
--forte-color-danger-hover | var(--forte-danger-10) |
--forte-color-danger-soft | var(--forte-danger-3) |
--forte-color-danger-text | var(--forte-danger-11) |
--forte-color-info | var(--forte-info-9) |
--forte-color-info-border | var(--forte-info-7) |
--forte-color-info-soft | var(--forte-info-3) |
--forte-color-info-text | var(--forte-info-11) |
--forte-color-on-danger | oklch(0.995 0 0) |
--forte-color-on-info | oklch(0.995 0 0) |
--forte-color-on-success | oklch(0.995 0 0) |
--forte-color-on-warning | oklch(0.145 0 0) |
--forte-color-success | var(--forte-success-9) |
--forte-color-success-border | var(--forte-success-7) |
--forte-color-success-soft | var(--forte-success-3) |
--forte-color-success-text | var(--forte-success-11) |
--forte-color-warning | var(--forte-warning-9) |
--forte-color-warning-border | var(--forte-warning-7) |
--forte-color-warning-soft | var(--forte-warning-3) |
--forte-color-warning-text | var(--forte-warning-11) |
Focus ring
The ring is brand-colored and still measured: the outline is --forte-color-primary-text, the ramp step the contrast harness holds to 4.5:1 for every seed, and it sits on a gap filled with the near-page -outer tone rather than on the control itself — so a solid primary button and a ghost one ring against the same known background (WCAG SC 1.4.11). The seed itself cannot promise that, which is why it is the caret and not the ring. It grows out of the control's edge while fading in, and shrinks back while fading out, on --forte-focus-ring-duration; the distance is --forte-focus-ring-travel, a travel token, so reduced motion keeps the fade and drops the movement.
| Token | Default | Notes |
|---|---|---|
--forte-color-focus-ring | var(--forte-focus-ring-inner) | |
--forte-focus-ring-duration | var(--forte-duration-fast) | |
--forte-focus-ring-ease | var(--forte-ease-standard) | |
--forte-focus-ring-inner | var(--forte-color-primary-text) | |
--forte-focus-ring-offset | 2px | |
--forte-focus-ring-outer | light-dark(oklch(0.995 0 0), oklch(0.178 0 0)) | |
--forte-focus-ring-travel | var(--forte-travel-sm) | |
--forte-focus-ring-width | 2px | retuned under prefers-contrast: more |
Spacing
An eight-step scale on a 4px base. Components use it for all internal padding and gaps.
| Token | Default |
|---|---|
--forte-space-1 | 0.25rem |
--forte-space-2 | 0.5rem |
--forte-space-3 | 0.75rem |
--forte-space-4 | 1rem |
--forte-space-5 | 1.5rem |
--forte-space-6 | 2rem |
--forte-space-7 | 2.5rem |
--forte-space-8 | 3rem |
Typography
| Token | Default |
|---|---|
--forte-font-mono | ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace |
--forte-font-sans | ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif |
--forte-font-size-1 | 0.75rem |
--forte-font-size-2 | 0.875rem |
--forte-font-size-3 | 1rem |
--forte-font-size-4 | 1.125rem |
--forte-font-size-5 | 1.25rem |
--forte-font-size-6 | 1.5rem |
--forte-font-weight-bold | 700 |
--forte-font-weight-medium | 500 |
--forte-font-weight-normal | 400 |
--forte-font-weight-semibold | 600 |
--forte-letter-spacing-normal | 0em |
--forte-letter-spacing-tight | -0.01em |
--forte-line-height-normal | 1.5 |
--forte-line-height-tight | 1.2 |
Radius
The numbered scale plus the three semantic radii components actually read. Prefer overriding the semantic ones — or better, a radius preset, which is the form that nests correctly.
| Token | Default | Notes |
|---|---|---|
--forte-radius-1 | 0.125rem | rewritten by the data-forte-radius presets |
--forte-radius-2 | 0.25rem | rewritten by the data-forte-radius presets |
--forte-radius-3 | 0.375rem | rewritten by the data-forte-radius presets |
--forte-radius-4 | 0.5rem | rewritten by the data-forte-radius presets |
--forte-radius-5 | 0.75rem | rewritten by the data-forte-radius presets |
--forte-radius-6 | 1rem | rewritten by the data-forte-radius presets |
--forte-radius-control | var(--forte-radius-4) | rewritten by the data-forte-radius presets |
--forte-radius-full | 9999px | rewritten by the data-forte-radius presets |
--forte-radius-pill | var(--forte-radius-full) | rewritten by the data-forte-radius presets |
--forte-radius-surface | var(--forte-radius-5) | rewritten by the data-forte-radius presets |
Controls and density
Control heights, padding and the surface metrics — the group the density presets rewrite. The --forte-app-bar-h-* heights are here too: an AppBar is exactly that tall at each size, and the token exists so the layout around one — a sticky sidebar's offset, a page's scroll-padding-top — can read it.
| Token | Default | Notes |
|---|---|---|
--forte-app-bar-h-lg | calc(4.25rem + 1px) | rewritten by the data-forte-density presets |
--forte-app-bar-h-md | calc(3.75rem + 1px) | rewritten by the data-forte-density presets |
--forte-app-bar-h-sm | calc(2.75rem + 1px) | rewritten by the data-forte-density presets |
--forte-control-gap | var(--forte-space-2) | rewritten by the data-forte-density presets |
--forte-control-h-lg | 2.75rem | rewritten by the data-forte-density presets |
--forte-control-h-md | 2.25rem | rewritten by the data-forte-density presets |
--forte-control-h-sm | 1.75rem | rewritten by the data-forte-density presets |
--forte-control-px-lg | var(--forte-space-5) | rewritten by the data-forte-density presets |
--forte-control-px-md | var(--forte-space-4) | rewritten by the data-forte-density presets |
--forte-control-px-sm | var(--forte-space-3) | rewritten by the data-forte-density presets |
--forte-list-item-py | var(--forte-space-2) | rewritten by the data-forte-density presets |
--forte-surface-p | var(--forte-space-5) | rewritten by the data-forte-density presets |
--forte-target-comfortable | 44px | |
--forte-target-min | 24px |
Elevation
| Token | Default | Notes |
|---|---|---|
--forte-glass-alpha | 0.72 | retuned under prefers-reduced-transparency |
--forte-scrim-blur | 4px | retuned under prefers-reduced-transparency |
--forte-scrim-color | light-dark(oklch(0 0 0 / 0.45), oklch(0 0 0 / 0.65)) | retuned under prefers-contrast: more · retuned under prefers-reduced-transparency |
--forte-shadow-1 | 0 1px 2px 0 light-dark(oklch(0 0 0 / 0.05), oklch(0 0 0 / 0.4)) | retuned under prefers-contrast: more |
--forte-shadow-2 | 0 1px 3px 0 light-dark(oklch(0 0 0 / 0.1), oklch(0 0 0 / 0.5)), 0 1px 2px -1px light-dark(oklch(0 0 0 / 0.1), oklch(0 0 0 / 0.5)) | retuned under prefers-contrast: more |
--forte-shadow-3 | 0 4px 6px -1px light-dark(oklch(0 0 0 / 0.1), oklch(0 0 0 / 0.5)), 0 2px 4px -2px light-dark(oklch(0 0 0 / 0.1), oklch(0 0 0 / 0.5)) | retuned under prefers-contrast: more |
--forte-shadow-4 | 0 12px 24px -6px light-dark(oklch(0 0 0 / 0.12), oklch(0 0 0 / 0.6)), 0 4px 8px -4px light-dark(oklch(0 0 0 / 0.1), oklch(0 0 0 / 0.5)) | retuned under prefers-contrast: more |
Motion
Durations, travels and scales. The reduced-motion behavior is built into the tokens themselves — under the OS preference or a data-forte-motion attribute, travels collapse to 0px and scales to 1 while durations only shorten — so consuming these tokens gives you correct reduced-motion handling without writing a media query. The spring easings (--forte-ease-spring-*) are sampled damped-oscillator curves; each pairs with its matching --forte-duration-spring-*, and truncating a spring with a shorter duration looks broken.
| Token | Default | Notes |
|---|---|---|
--forte-duration-fast | 160ms | collapsed under reduced motion |
--forte-duration-instant | 1ms | collapsed under reduced motion |
--forte-duration-loop-orbit | 6000ms | collapsed under reduced motion |
--forte-duration-loop-pulse | 1600ms | collapsed under reduced motion |
--forte-duration-loop-spin | 800ms | collapsed under reduced motion |
--forte-duration-loop-sweep | 1400ms | collapsed under reduced motion |
--forte-duration-move | 220ms | collapsed under reduced motion |
--forte-duration-normal | 240ms | collapsed under reduced motion |
--forte-duration-slow | 400ms | collapsed under reduced motion |
--forte-duration-spring-bouncy | 730ms | collapsed under reduced motion |
--forte-duration-spring-gentle | 700ms | collapsed under reduced motion |
--forte-duration-spring-precise | 410ms | collapsed under reduced motion |
--forte-duration-spring-snappy | 400ms | collapsed under reduced motion |
--forte-pulse-dip | 1 | collapsed under reduced motion |
--forte-scale-enter | 0.95 | collapsed under reduced motion |
--forte-scale-exit | 1.02 | collapsed under reduced motion |
--forte-scale-press | 0.97 | collapsed under reduced motion |
--forte-spin-turn | 1turn | collapsed under reduced motion |
--forte-travel-lg | 16px | collapsed under reduced motion |
--forte-travel-md | 8px | collapsed under reduced motion |
--forte-travel-page | 32px | collapsed under reduced motion |
--forte-travel-sm | 4px | collapsed under reduced motion |
--forte-travel-xs | 2px | collapsed under reduced motion |
For tooling
This same inventory ships in the package as machine-readable JSON — every token with its default, its typed registration where one exists, and every overriding selector:
import tokens from "@forte-ui/react/docs-data/tokens.json";These tables are rendered from that file, as is anything else that needs the full token surface — theme editors, lint rules, design-tool sync.