oriUI

Design tokens

Every oriUI package reads one set of design tokens. This page is the catalog — every token, and the resolution model that turns a raw value into the color 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. Color 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 are single-class (.ori-color_primary, .ori-button_lg), so every one of them weighs the same (0,1,0) and a block's own baked defaults sit at zero specificity in a :where() rule beneath them — layer order and one flat class, 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.

Color — 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- color (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 colors 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-warning#f59e0b
--ori-color-on-warningvar(--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_warningvar(--ori-color-warning)var(--ori-color-on-warning)var(--ori-color-warning-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 color chain reads: neutral ramp / role source → active alias → --ori-color / --ori-color-text → the component. Switching a color 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 warning 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 color as the fill, just darker (light theme) or lighter (dark theme) — one hue, only lightness varies, not the muddier, different-looking color 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)
warningoklch(from var(--ori-color-warning) min(l, 0.42) c h)oklch(from var(--ori-color-warning) 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 soft 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 color.

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, color and variant compose freely: any color × any variant, no extra rules.

There is no paired .ori-variant base class — each .ori-variant_* sets the whole group on its own, and a block without one keeps the defaults it bakes in. Each .ori-variant_* repoints the group:

Variantbackgroundtextborderopacity
_solidvar(--ori-color)var(--ori-color-on)transparent1
_softcolor-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
_quiettransparentvar(--ori-color-text)transparent0.85

solid 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 color, 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 behavior 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 is declared at :root as var(--ori-size-action_inherit) (1em, font-driven), but no action component reads that default: every block bakes its own step on itself (.ori-button { --ori-size-action: var(--ori-size-action_md) }), and the .ori-<family>_<step> utility beats it by layer order. Two consequences worth knowing: the text default only shows up on markup that opts into it, and repointing --ori-size-action on a wrapper does nothing — scope a subtree through the raw step instead (see Customization §5).

Token / stepValue
--ori-size-action_inherit1em
--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_inherit) (0).

Token / stepValue
--ori-size-action-space_inherit0
--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_none0
--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_full9999px

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_none0
--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_inherit) (1em, i.e. inherit).

Token / stepValue (computed)
--ori-font-size_inherit1em
--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).

Structure — outline tokens

Borders, dividers and control edges are not a color ROLE — they are structure, and they used to be invented per component: forty-odd ad-hoc color-mix percentages, so a consumer who wanted one consistent hairline had nothing to read and had to guess our numbers.

There are exactly two weights, because the library has exactly two structural jobs:

TokenValueUsed for
--ori-color-outlinecolor-mix(in srgb, currentcolor 12%, transparent)The resting hairline: panel and menu borders, dividers, list separators
--ori-color-outline-strongcolor-mix(in srgb, currentcolor 28%, transparent)The control edge that says "this is interactive": text fields, key caps

Both derive from currentcolor, and that is the whole trick: a custom property substitutes where the var() is used, not where it is declared, so a hairline takes the color of the text it accompanies and a themed subtree gets the right value with no per-theme re-declaration.

Repoint them like any other token — one line, no override fight:

css
:root {
    /* a warmer, slightly heavier hairline everywhere */
    --ori-color-outline: color-mix(in srgb, #7a5c3e 18%, transparent);
}

What these tokens deliberately do not cover: the low-percentage background tints behind a hover row, a zebra stripe or a progress track. Those are state tinting, a different axis, and folding them in would make the name lie. The one other exception is the checkbox and radio box edge, which is a third, heavier weight on purpose — an unfilled interactive target has to read as an affordance.

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 one reskin reaches all of them.

See also