oriUI

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:

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

TokenValue
--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*-lighton-*-light*-darkon-*-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:

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

TokenValue
--ori-color-success#15803d
--ori-color-on-success#ffffff
--ori-color-warn#f59e0b
--ori-color-on-warnvar(--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:

css
--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_primaryvar(--ori-color-primary)var(--ori-color-on-primary)var(--ori-color-primary-text)
.ori-color_secondaryvar(--ori-color-secondary)var(--ori-color-on-secondary)var(--ori-color-secondary-text)
.ori-color_surfacevar(--ori-color-surface)var(--ori-color-on-surface)var(--ori-color-on-surface)
.ori-color_backgroundvar(--ori-color-background)var(--ori-color-on-background)var(--ori-color-on-background)
.ori-color_successvar(--ori-color-success)var(--ori-color-on-success)var(--ori-color-success-text)
.ori-color_warnvar(--ori-color-warn)var(--ori-color-on-warn)var(--ori-color-warn-text)
.ori-color_dangervar(--ori-color-danger)var(--ori-color-on-danger)var(--ori-color-danger-text)
.ori-color_infovar(--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_primaryori-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
primaryoklch(from var(--ori-color-primary) min(l, 0.42) c h)oklch(from var(--ori-color-primary) max(l, 0.86) c h)
secondaryoklch(from var(--ori-color-secondary) min(l, 0.42) c h)oklch(from var(--ori-color-secondary) max(l, 0.86) c h)
successoklch(from var(--ori-color-success) min(l, 0.42) c h)oklch(from var(--ori-color-success) max(l, 0.86) c h)
warnoklch(from var(--ori-color-warn) min(l, 0.42) c h)oklch(from var(--ori-color-warn) max(l, 0.86) c h)
dangeroklch(from var(--ori-color-danger) min(l, 0.42) c h)oklch(from var(--ori-color-danger) max(l, 0.86) c h)
infooklch(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:

Variantbackgroundtextborderopacity
_fillvar(--ori-color)var(--ori-color-on)transparent1
_tonalcolor-mix(in srgb, var(--ori-color), transparent 75%)var(--ori-color-text)transparent1
_outlinetransparentvar(--ori-color-text)var(--ori-color-text)1
_texttransparentvar(--ori-color-text)transparent1
_plaintransparentvar(--ori-color-text)transparent0.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 / stepValue
--ori-size-action_text1em
--ori-size-action_xs1.25rem
--ori-size-action_sm1.5rem
--ori-size-action_md2.75rem
--ori-size-action_lg3rem
--ori-size-action_xl3.75rem
--ori-size-action_xxl4.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 / stepValue
--ori-size-action-space_text0
--ori-size-action-space_xs0.625rem
--ori-size-action-space_sm0.75rem
--ori-size-action-space_md0.625rem
--ori-size-action-space_lg0.75rem
--ori-size-action-space_xl0.625rem
--ori-size-action-space_xxl0.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 / stepValue (computed)
--ori-size-radius_zero0
--ori-size-radius_xs0.125rem
--ori-size-radius_sm0.25rem
--ori-size-radius_md0.5rem
--ori-size-radius_lg0.75rem
--ori-size-radius_xl1rem
--ori-size-radius_rounded9999px

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 / stepValue (computed)
--ori-size-gap_zero0
--ori-size-gap_xs0.125rem
--ori-size-gap_sm0.25rem
--ori-size-gap_md0.5rem
--ori-size-gap_lg0.75rem
--ori-size-gap_xl1rem
--ori-size-gap_xxl1.5rem (no class)

Screen (breakpoints)

Pure constants for @media queries and consumer use — no alias, no utility class. You read these directly.

TokenValue
--ori-size-screen_xs600px
--ori-size-screen_sm840px
--ori-size-screen_md1200px
--ori-size-screen_lg1600px
--ori-size-screen_xl1920px
--ori-size-screen_xxl2560px

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 / stepValue (computed)
--ori-font-size_text1em
--ori-font-size_xs0.75rem
--ori-font-size_sm0.875rem
--ori-font-size_md1rem
--ori-font-size_lg1.125rem
--ori-font-size_xl1.25rem
--ori-font-size_xxl1.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)):

TokenValue
--ori-shadow-sm0 1px 2px (neutral-900 8%)
--ori-shadow-md0 1px 2px (neutral-900 6%), 0 6px 16px -4px (neutral-900 12%)
--ori-shadow-lg0 2px 4px (neutral-900 6%), 0 14px 36px -8px (neutral-900 18%)
--ori-shadow-ring0 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:

TokenValue
--ori-shadow-sm0 1px 2px (#000 45%)
--ori-shadow-md0 1px 2px (#000 45%), 0 8px 24px -6px (#000 60%)
--ori-shadow-lg0 2px 4px (#000 45%), 0 16px 40px -8px (#000 70%)
--ori-shadow-ring0 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