Design tokens
oriUI is three independent layers woven around one set of design tokens. This page is the catalog — every token, and the resolution model that turns a raw value into the colour a component paints. The chain is short and deliberate: a primitive feeds a semantic role, a role feeds a per-instance alias, and components read only the alias. That indirection is what lets a single class repoint a single token — the whole zero-runtime theming story rests on it.
Two axes resolve slightly differently. Colour is three-tier (primitive → role → alias);
size, radius, gap, and font-size are two-tier (raw scale → alias). Everything here is plain CSS
custom properties under @layer — no JavaScript, no build step.
This page is the reference. To switch skin and mode see Theming; to override or brand see Customization; for the standalone class story see Using the CSS layer.
The cascade — @layer order
Every token and utility lives inside a layer, declared once in source order:
@layer ori.reset, ori.tokens, ori.base, ori.components, ori.utilities;
Later layers win. ori.tokens holds the raw scales and role tokens at :root; ori.utilities
holds the .ori-*_<step> classes that repoint aliases. Because the order is fixed and explicit,
the cascade is predictable and any unlayered consumer style wins over all of it — overriding is
trivial, never a !important fight. The utility modifiers use a flat compound selector
(.ori-color.ori-color_primary), so their specificity stays low and consistent — layer order, not a
specificity contest, is what decides the cascade.
The contract for components is one line: read the resolved alias, never a raw scale token, never a hardcoded hex.
Colour — three tiers
Tier 1 — primitives: the neutral ramp
A single slate-like ramp is the source of truth for neutral surfaces and text. Role tokens and skins reference it instead of duplicating hex, so one neutral lives in exactly one place.
| Token | Value |
|---|---|
--ori-neutral-50 | #f8fafc |
--ori-neutral-100 | #f1f5f9 |
--ori-neutral-200 | #e2e8f0 |
--ori-neutral-300 | #cbd5e1 |
--ori-neutral-400 | #94a3b8 |
--ori-neutral-500 | #64748b |
--ori-neutral-600 | #475569 |
--ori-neutral-700 | #334155 |
--ori-neutral-800 | #1e293b |
--ori-neutral-900 | #0f172a |
--ori-neutral-950 | #020617 |
Tier 2 — semantic role tokens (light/dark-aware)
Each role ships four source tokens — a light and dark value, each paired with a contrast-checked
on- colour (the legible foreground for text on that surface). The on- pairing is not decorative:
it is asserted by an executable WCAG-AA contrast test, so a filled surface always has readable text.
Values below are the default Ori skin.
| Role | *-light | on-*-light | *-dark | on-*-dark |
|---|---|---|---|---|
| primary | #0369a1 | #ffffff | #38bdf8 | #06131f |
| secondary | #e0f2fe | #0c4a6e | #14384e | #e6f3fb |
| surface | #ffffff | #0a2233 | #0e2536 | #e6f3fb |
| background | #f3f8fc | #0a2233 | #06131f | #e6f3fb |
Above those sit active aliases — declared on :root and defaulting to the light source. These
are the "currently selected" role value:
--ori-color-primary: var(--ori-color-primary-light);
--ori-color-on-primary: var(--ori-color-on-primary-light);
/* …secondary, surface, background, each defaulting to its -light source */
Each alias defaults to its *-light source; flipping mode repoints all eight at once to the matching
*-dark (or back) source, and a skin overrides only those source tokens for the four roles — so a
component reading the alias never changes. (How to flip mode and skin →
Theming.)
Status tokens — shared, own hues
Status colours carry their own meaning across every theme. They are not repointed by mode and
not overridden by skins — success is green everywhere.
| Token | Value |
|---|---|
--ori-color-success | #15803d |
--ori-color-on-success | #ffffff |
--ori-color-warn | #f59e0b |
--ori-color-on-warn | var(--ori-neutral-950) |
--ori-color-danger | #b91c1c |
--ori-color-on-danger | #ffffff |
--ori-color-info | #1d4ed8 |
--ori-color-on-info | #ffffff |
Tier 3 — the per-instance component alias
Components never read a role token directly. They read three resolved aliases — --ori-color
(the surface), --ori-color-on (its legible foreground when the role is painted as a fill), and
--ori-color-text (its legible foreground when the role is painted as text on the surrounding
surface, covered next). All three default to a neutral, role-agnostic value so non-skinned use (an
avatar, an icon, bare text) stays sensible — currentColor for the first two, the on-surface ink for
the third:
--ori-color: currentColor;
--ori-color-on: currentColor;
--ori-color-text: var(--ori-color-on-surface);
A role-specific --ori-color-text (below) is layered on by the .ori-color_* utility, or by a block
that bakes a role (Button, Tabs, Tag, …) — this :root default only ever surfaces for bare,
un-skinned usage.
A .ori-color_* utility class is what binds that triplet to a role — the same base class plus a
scale value pair (ori-color + ori-color_<name>) that the component pages use. Each sets all
three aliases at once:
| Class | --ori-color → | --ori-color-on → | --ori-color-text → |
|---|---|---|---|
.ori-color_primary | var(--ori-color-primary) | var(--ori-color-on-primary) | var(--ori-color-primary-text) |
.ori-color_secondary | var(--ori-color-secondary) | var(--ori-color-on-secondary) | var(--ori-color-secondary-text) |
.ori-color_surface | var(--ori-color-surface) | var(--ori-color-on-surface) | var(--ori-color-on-surface) |
.ori-color_background | var(--ori-color-background) | var(--ori-color-on-background) | var(--ori-color-on-background) |
.ori-color_success | var(--ori-color-success) | var(--ori-color-on-success) | var(--ori-color-success-text) |
.ori-color_warn | var(--ori-color-warn) | var(--ori-color-on-warn) | var(--ori-color-warn-text) |
.ori-color_danger | var(--ori-color-danger) | var(--ori-color-on-danger) | var(--ori-color-danger-text) |
.ori-color_info | var(--ori-color-info) | var(--ori-color-on-info) | var(--ori-color-info-text) |
So the full colour chain reads: neutral ramp / role source → active alias → --ori-color /
--ori-color-text → the component. Switching a colour is one class swap — ori-color_primary →
ori-color_danger — and every layer above resolves through var() with nothing recomputed.
All eight role pairs resolving live — each swatch is the same .ori-color_* utility, only the role
changes. (For the per-component usage of these classes, see Button.)
Text — the on-surface foreground
A role's --ori-color-<role> hue is engineered as a fill background — light or saturated,
paired with a legible --ori-color-on-<role> ink for text painted on that fill. Used the other
way round — as text sitting on the surrounding surface (the non-fill button variants, the
selected tab, alert, tag) — a light or saturated role can drop below the WCAG AA 4.5:1 body-text bar
(amber warn is the worst case). --ori-color-<role>-text is the dedicated on-surface tone for
exactly that — a relative color that keeps the role's own hue and chroma and clamps only its
lightness, so the text reads as the same colour as the fill, just darker (light theme) or lighter
(dark theme) — one hue, only lightness varies, not the muddier, different-looking colour a mix toward
a neutral ink would give:
| Role | --ori-color-<role>-text — light default | --ori-color-<role>-text — dark override |
|---|---|---|
| primary | oklch(from var(--ori-color-primary) min(l, 0.42) c h) | oklch(from var(--ori-color-primary) max(l, 0.86) c h) |
| secondary | oklch(from var(--ori-color-secondary) min(l, 0.42) c h) | oklch(from var(--ori-color-secondary) max(l, 0.86) c h) |
| success | oklch(from var(--ori-color-success) min(l, 0.42) c h) | oklch(from var(--ori-color-success) max(l, 0.86) c h) |
| warn | oklch(from var(--ori-color-warn) min(l, 0.42) c h) | oklch(from var(--ori-color-warn) max(l, 0.86) c h) |
| danger | oklch(from var(--ori-color-danger) min(l, 0.42) c h) | oklch(from var(--ori-color-danger) max(l, 0.86) c h) |
| info | oklch(from var(--ori-color-info) min(l, 0.42) c h) | oklch(from var(--ori-color-info) max(l, 0.86) c h) |
oklch(from <color> L c h) is CSS relative-color syntax: it derives a new color from an existing one,
carrying forward whichever channels you name bare. Naming c and h (chroma, hue) bare keeps them
exactly as the role's; only L (lightness) is replaced — clamped to min(l, 0.42) in light (never
lighter than 0.42) or max(l, 0.86) in dark (never darker than 0.86). Those are the tightest bounds
that clear WCAG AA (≥ 4.5:1) for every role × skin × text kind, including the tonal hover/active tint
(min ~4.55:1) — verified in real Chromium by e2e/text-contrast.spec.ts.
surface and background don't get their own -text token — their on- pair already is the
on-surface ink, so .ori-color_surface / .ori-color_background point --ori-color-text straight
at --ori-color-on-surface / --ori-color-on-background (see the table above).
The formula is declared twice — once at :root (the light default, shown above) and again inside
the dark theme selectors (:root.dark, .ori-theme_dark, with the opposite clamp) — so it tracks the
active theme automatically. Because both declarations read var(--ori-color-<role>), the tone
also tracks any skin's role override, or a brand's own override, at any nesting — a custom skin
gets an AA-safe text tone for free, no extra work required. (To override it — globally, per skin, or
per instance — see Customization.)
The .ori-variant_outline border (below) reads this same -text tone instead of the raw role, so an
outline button's border and label read as one colour.
Variants
A variant is a small token group — border, opacity, background, text — set from the resolved
--ori-color / --ori-color-on / --ori-color-text triplet. Because variants read the alias,
colour and variant compose freely: any colour × any variant, no extra rules.
The base .ori-variant defaults to a transparent, full-opacity surface inheriting currentColor.
Each .ori-variant_* repoints the group:
| Variant | background | text | border | opacity |
|---|---|---|---|---|
_fill | var(--ori-color) | var(--ori-color-on) | transparent | 1 |
_tonal | color-mix(in srgb, var(--ori-color), transparent 75%) | var(--ori-color-text) | transparent | 1 |
_outline | transparent | var(--ori-color-text) | var(--ori-color-text) | 1 |
_text | transparent | var(--ori-color-text) | transparent | 1 |
_plain | transparent | var(--ori-color-text) | transparent | 0.5 |
fill keeps --ori-color-on — the ink tuned for its own solid background. Every non-fill mapping
instead reads --ori-color-text for its label, the AA-safe on-surface tone from
Text — the on-surface foreground above, because at those
opacities/borders the role is painted as text on the page, not as a fill. outline's border reads
that same tone instead of the raw --ori-color, so the ring and the label read as one colour, and it
clears the WCAG 1.4.11 non-text 3:1 minimum for pale roles too.
Interaction state is derived from these same tokens, not stored — hover and [data-active] deepen
the color-mix per variant. (See the live behaviour on Button.)
Sizes — two tiers
The size axis drops the role tier. A family has a raw scale of fixed values and a single
resolved alias that components read; a .ori-<family>_<step> utility class repoints the alias
to one scale value. That is the entire pattern — raw scale → alias → component, switched by a
class.
Action (control height)
md is 2.75rem — 44px at the browser-default 16px root, meeting the iOS HIG ≥44pt and Android
Material ≥48dp touch-target minimums; xs / sm are compact opt-ins for dense or icon-only UI. The
alias --ori-size-action defaults to var(--ori-size-action_text) (1em, i.e. font-driven).
| Token / step | Value |
|---|---|
--ori-size-action_text | 1em |
--ori-size-action_xs | 1.25rem |
--ori-size-action_sm | 1.5rem |
--ori-size-action_md | 2.75rem |
--ori-size-action_lg | 3rem |
--ori-size-action_xl | 3.75rem |
--ori-size-action_xxl | 4.25rem |
Action-space (control padding)
Padding scale paired with action height; alias --ori-size-action-space defaults to
var(--ori-size-action-space_text) (0).
| Token / step | Value |
|---|---|
--ori-size-action-space_text | 0 |
--ori-size-action-space_xs | 0.625rem |
--ori-size-action-space_sm | 0.75rem |
--ori-size-action-space_md | 0.625rem |
--ori-size-action-space_lg | 0.75rem |
--ori-size-action-space_xl | 0.625rem |
--ori-size-action-space_xxl | 0.75rem |
Radius
Corners, computed from md (0.5rem) by a 0.125rem step. Alias --ori-size-radius defaults to
var(--ori-size-radius_md). Like the other scales, the raw values live at :root; the
.ori-size-radius_* utilities (in ori.utilities) only repoint the alias. There is no _xxl.
| Token / step | Value (computed) |
|---|---|
--ori-size-radius_zero | 0 |
--ori-size-radius_xs | 0.125rem |
--ori-size-radius_sm | 0.25rem |
--ori-size-radius_md | 0.5rem |
--ori-size-radius_lg | 0.75rem |
--ori-size-radius_xl | 1rem |
--ori-size-radius_rounded | 9999px |
Gap
Spacing between items, same md/step derivation as radius. Alias --ori-size-gap defaults to
var(--ori-size-gap_md). The _xxl raw token (1.5rem) exists but has no repointing utility
class — it is reachable only by reading the token directly.
| Token / step | Value (computed) |
|---|---|
--ori-size-gap_zero | 0 |
--ori-size-gap_xs | 0.125rem |
--ori-size-gap_sm | 0.25rem |
--ori-size-gap_md | 0.5rem |
--ori-size-gap_lg | 0.75rem |
--ori-size-gap_xl | 1rem |
--ori-size-gap_xxl | 1.5rem (no class) |
Screen (breakpoints)
Pure constants for @media queries and consumer use — no alias, no utility class. You read these
directly.
| Token | Value |
|---|---|
--ori-size-screen_xs | 600px |
--ori-size-screen_sm | 840px |
--ori-size-screen_md | 1200px |
--ori-size-screen_lg | 1600px |
--ori-size-screen_xl | 1920px |
--ori-size-screen_xxl | 2560px |
Fonts — font-size
The type scale follows the same two-tier pattern, derived from md (1rem) by a 0.125rem step. The
raw scale lives at :root and the .ori-font-size_* utilities repoint the alias; --ori-font-size
defaults to var(--ori-font-size_text) (1em, i.e. inherit).
| Token / step | Value (computed) |
|---|---|
--ori-font-size_text | 1em |
--ori-font-size_xs | 0.75rem |
--ori-font-size_sm | 0.875rem |
--ori-font-size_md | 1rem |
--ori-font-size_lg | 1.125rem |
--ori-font-size_xl | 1.25rem |
--ori-font-size_xxl | 1.375rem |
Nothing pins the root: the size/font scales are rem, so they resolve against the user's browser
font-size setting (16px by default — at which the scales compute to their historical px values).
Elevation — shadow tokens
Soft, cool-tinted shadows, theme-aware. Light tints with the neutral ink ramp at low alpha; dark leans on deeper black plus a hairline highlight ring. Components read these directly.
Light (:root) — tinted with the neutral ink ramp at low alpha (color-mix(in srgb, …, transparent)):
| Token | Value |
|---|---|
--ori-shadow-sm | 0 1px 2px (neutral-900 8%) |
--ori-shadow-md | 0 1px 2px (neutral-900 6%), 0 6px 16px -4px (neutral-900 12%) |
--ori-shadow-lg | 0 2px 4px (neutral-900 6%), 0 14px 36px -8px (neutral-900 18%) |
--ori-shadow-ring | 0 0 0 1px (neutral-900 8%) |
Dark (:root.dark, .ori-theme_dark) — deeper #000000 shadow at higher alpha, and a #ffffff 9%
hairline highlight ring instead of an ink ring:
| Token | Value |
|---|---|
--ori-shadow-sm | 0 1px 2px (#000 45%) |
--ori-shadow-md | 0 1px 2px (#000 45%), 0 8px 24px -6px (#000 60%) |
--ori-shadow-lg | 0 2px 4px (#000 45%), 0 16px 40px -8px (#000 70%) |
--ori-shadow-ring | 0 0 0 1px (#fff 9%) |
(Alpha pairs are shorthand for color-mix(in srgb, <color> <pct>, transparent) — see the styles
source for the literal declarations.)
Why this enables zero-runtime theming
Every value above resolves through var() indirection, and components read only the resolved
alias — so the whole catalog is repointed by a class or attribute toggle, never by JavaScript. The
zero-runtime mechanics that ride on that — flipping mode, switching skins, and one-off overrides —
live in Theming and Customization. The same tokens feed
the styled Vue components, the standalone CSS layer, and the
headless core, so you can move between layers, or reskin all of them, without a
rewrite.
See also
- Theming — switch whole palettes and modes, and author a new named skin.
- Customization — one-off token overrides and per-instance tweaks, without authoring a whole skin.
- Using the CSS layer — applying these tokens with the standalone
.ori-*classes in htmx, Astro, or plain HTML. - Get started · Button · Headless core