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:
@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.
| 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- 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 | *-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 colors 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-warning | #f59e0b |
--ori-color-on-warning | 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_warning | var(--ori-color-warning) | var(--ori-color-on-warning) | var(--ori-color-warning-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 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 |
|---|---|---|
| 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) |
| warning | oklch(from var(--ori-color-warning) min(l, 0.42) c h) | oklch(from var(--ori-color-warning) 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 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:
| Variant | background | text | border | opacity |
|---|---|---|---|---|
_solid | var(--ori-color) | var(--ori-color-on) | transparent | 1 |
_soft | 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 |
_quiet | transparent | var(--ori-color-text) | transparent | 0.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 / step | Value |
|---|---|
--ori-size-action_inherit | 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_inherit) (0).
| Token / step | Value |
|---|---|
--ori-size-action-space_inherit | 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_none | 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_full | 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_none | 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_inherit) (1em, i.e. inherit).
| Token / step | Value (computed) |
|---|---|
--ori-font-size_inherit | 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).
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:
| Token | Value | Used for |
|---|---|---|
--ori-color-outline | color-mix(in srgb, currentcolor 12%, transparent) | The resting hairline: panel and menu borders, dividers, list separators |
--ori-color-outline-strong | color-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:
: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)):
| 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 one reskin reaches all of them.
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