Token Reference
The tokens the library ships, layer by layer: color ramps, spacing, typography, radius, motion and depth. Look values up here; write and switch themes on the Themes page.
Token Architecture
Three layers. The foundation holds the raw ramps a theme re-tints; the semantic layer maps them to roles like surface-elevated and resolves both modes via light-dark(); the interaction layer times motion and stacks depth. How to write, scope and
switch a theme is the Themes page; this one lists what exists.
Foundation Colors
@theme {
/* Foundation Colors (oklch for better perception) */
--color-neutral-50: oklch(0.965 0.006 240);
--color-neutral-500: oklch(0.55 0.016 240);
--color-neutral-900: oklch(0.15 0.012 240);
/* Intent 500s darkened so white text passes WCAG AA */
--color-primary-500: oklch(0.58 0.15 240);
--color-success-500: oklch(0.5 0.15 140);
--color-danger-500: oklch(0.5 0.17 25);
}Semantic Tokens
/* Semantic Layer – maps foundation to
roles; light-dark() resolves both modes */
--color-surface-base: light-dark(
var(--color-neutral-0), var(--color-neutral-900));
--color-text-primary: light-dark(
var(--color-neutral-900), var(--color-neutral-100));
--color-primary: light-dark(
var(--color-primary-600), var(--color-primary-500));
/* Interactive states */
--color-interactive-hover:
oklch(from var(--color-primary-500) l c h / 0.1);
--color-interactive-focus: light-dark(
var(--color-primary-500), var(--color-primary-400));Interaction Layer
/* Interaction Layer – motion & depth */
--blocks-duration-fast: 150ms;
--blocks-duration-normal: 250ms;
--blocks-ease-gentle: cubic-bezier(0.25, 0.1, 0.25, 1);
--blocks-shadow-sm: var(--blocks-shadow-scale-sm);
--blocks-shadow-md: var(--blocks-shadow-scale-md);Override at the right layer
- Re-tint a hue → foundation ramp. Override every stop of the ramp, keeping each stop's lightness/chroma profile: the WCAG-tuned contrast survives, and the semantic layer picks the new hue up in both modes automatically. This is exactly what the shipped themes do.
- Change a role → semantic token. To alter what
"elevated" or "subtle" means, override the semantic token itself, and supply a
light-dark(light, dark)pair, otherwise the token is pinned to one look in both modes. - Restyle one component → the component API, not CSS. That is a job for
class,slotClassesand BlocksProvider, never for!color overrides; the Customization hub carries the decision table.
Custom theming and dark mode used to live on this page. They moved to Themes → Write Your Own Theme and Themes → Dark Mode.
Color System
Neutral Palette
The neutral ramp has 16 steps: alongside 0 (pure white)
and the standard 50–950 ladder it ships a 25 tint (the
quiet-surface ground) and the half-steps 650/750/850,
which differentiate the dark-mode elevation ladder.
Intent Ramps
Six intent ramps (primary, secondary, success, warning, danger, info), each a full 50–950
ladder. The ramps are tuned so text-on-fill clears WCAG AA on every solid fill: white on the 500/600/700 fills in light mode, near-black on
the lighter 400/500 fills in dark mode. It is the label colour for every solid intent, which is why it is not named after one of them. text-on-primary still exists, resolves to the same value and governs the primary fill alone, so retuning it cannot
repaint success or danger along with it. Warning is the deliberate exception (its fill stays light
in both modes and pairs with its own warm-dark text-on-warning).
primary
secondary
success
warning
danger
info
Intent Roles
The primary intent carries 6 roles, listed below; every other intent carries them
under the same suffixes, and the gaps are listed under the table.
Solid things (a button, a badge, a progress bar) take the base fill, bg-primary, with text-text-on-fill as the label on it. Text in the intent's colour on a reading surface (a page, a card, a panel) or
on the intent's own -subtle tint takes text-primary-text,
the stop that clears AA there. Tailwind also emits text-primary from
the fill token and nothing flags it; as text it lands below AA (4.28:1 on bg-primary-subtle), so write text-primary-text. text-primary-emphasis is the near-ink step for a heading or a strong statement. The two classes that look alike do opposite
jobs: text-primary-text is text in the primary colour, text-text-on-fill (or its primary-only alias text-text-on-primary) is the label on a primary fill. The swatches render in whatever theme is active, on this
site the docs skin's own primary; the light and dark stops under each token are the library's
own.
| Token | Utility | Purpose |
|---|---|---|
| --color-primary light primary-600 (L 0.52) · dark primary-500 (L 0.58) | bg-primary | the fill — a surface with text-on-fill sitting on it; never a text colour |
| --color-primary-hover light primary-700 (L 0.44) · dark primary-400 (L 0.66) | bg-primary-hover | the hover step of the fill |
| --color-primary-active light primary-800 (L 0.36) · dark primary-300 (L 0.74) | bg-primary-active | the pressed step of the fill |
| --color-primary-subtle light primary-50 (L 0.95) · dark primary-900 (L 0.28) | bg-primary-subtle | a soft tinted ground in the intent's hue — an Alert's background, a selected row |
| --color-primary-text light primary-700 (L 0.44) · dark primary-400 (L 0.66) | text-primary-text | the intent as text on a reading surface — the nearest stop that clears AA 4.5:1 on every reading surface and the intent's own -subtle |
| --color-primary-emphasis light primary-900 (L 0.28) · dark primary-200 (L 0.82) | bg-primary-emphasistext-primary-emphasis | the near-ink tier — strong statements as text, and doubles as a fill |
neutralhas no-texttoken: its base already clears AA as text on every ground in every theme, so a -text role would only alias the basewarning-text: the light stop is -800, the only rung of a yellow ramp that reads on white — it lands amber-brown while the fill stays yellowdanger-text: the light stop is the fill's stop, red already reads on paper; dark needs -300 because sRGB red is too dim on the lightened panels at -400
Run urbicon css-reference intents (the urbicon CLI) for the
stops of every other intent.
Neutral Intent Chrome
The neutral intent (bg-neutral, text-neutral,
neutral borders) is built from a separate --color-warm-neutral-* ramp, but never renders it directly: each role re-derives the ramp stop through oklch(from … l c var(--neutral-chrome-hue)), keeping the ramp's lightness and chroma and taking only the hue from the knob. The knob
defaults to 240, so the library's own chrome is cool, and re-tinting it is a one-line theme
move (see Motion & Depth). The swatches below render in whatever theme is active: this
docs site re-pins them to the warm ramp directly, which is why they look warm here.
Your own color tokens live in the same system: any --color-* variable declared in @theme gets its Tailwind
utilities for free.
Custom brand token
@theme {
/* bg-brand-500, text-brand-500, border-brand-500, … */
--color-brand-500: oklch(0.5 0.25 45);
}Spacing Scale
Urbicon UI does not ship a custom spacing token layer. It uses Tailwind's built-in spacing scale directly. Apply spacing with the standard utilities (p-4, gap-2, m-6). Each step is 0.25rem × n,
driven by Tailwind's own --spacing variable.
| Utility | Value | Pixels | Visual |
|---|---|---|---|
| p-0 / gap-0 | 0 | 0px | |
| p-1 / gap-1 | 0.25rem | 4px | |
| p-2 / gap-2 | 0.5rem | 8px | |
| p-3 / gap-3 | 0.75rem | 12px | |
| p-4 / gap-4 | 1rem | 16px | |
| p-6 / gap-6 | 1.5rem | 24px | |
| p-8 / gap-8 | 2rem | 32px | |
| p-12 / gap-12 | 3rem | 48px |
Typography Scale
Sizes, weights, leading, tracking and families are Tailwind @theme variables;
override them alongside your color ramps (Themes → Typography, including the paired --text-*--line-height rule). The two sub-xs steps are library-added and ship size-only; every other row re-tunes a Tailwind
built-in.
| Utility | Override | Value | Pixels | Uses in blocks | Example |
|---|---|---|---|---|---|
| text-3xs | --text-3xs | 0.625rem | 10px | 17 | The quick brown fox |
| text-2xs | --text-2xs | 0.6875rem | 11px | 23 | The quick brown fox |
| text-xs | --text-xs | 0.75rem | 12px | 130 | The quick brown fox |
| text-sm | --text-sm | 0.875rem | 14px | 170 | The quick brown fox |
| text-base | --text-base | 1rem | 16px | 96 | The quick brown fox |
| text-lg | --text-lg | 1.125rem | 18px | 39 | The quick brown fox |
| text-xl | --text-xl | 1.25rem | 20px | 14 | The quick brown fox |
| text-2xl | --text-2xl | 1.5rem | 24px | 5 | The quick brown fox |
Weights
| Utility | Override | Value | Uses in blocks |
|---|---|---|---|
| font-normal | --font-weight-normal | 400 | 6 |
| font-medium | --font-weight-medium | 500 | 71 |
| font-semibold | --font-weight-semibold | 600 | 53 |
| font-bold | --font-weight-bold | 700 | 12 |
Border Radius
Physical Scale
Real --radius-* CSS variables, each with a matching Tailwind utility.
Semantic Tiers
Components consume a 3-tier semantic vocabulary, not raw radii. Re-tint the three tier
variables to reshape the whole library at once (the full cascade: Radius Tiers). A fourth token, --radius-bridge,
is not a tier: it pairs pill triggers with their dropdown panels.
| Token | Value | Used by |
|---|---|---|
| --radius-commit | 9999px | Pill/round — actions, identity, status (Button, Badge, Toggle) |
| --radius-control | 9999px | Radio indicator only. Defaults to the same pill as --radius-commit but is not tied to it, so squaring your buttons keeps the "pick exactly one" circle. Set it to 0 if you want square radios too. |
| --radius-modify | var(--radius-sm) · 4px | Editable surfaces, navigation (Input, Select, Tab, Menu) |
| --radius-contain | var(--radius-xs) · 2px | Containers, panels (Card, Alert, Dialog, Drawer, Popover, Tooltip) |
| --radius-bridge | var(--radius-md) · 6px | The middle rung, for two cases: adjacency (a floating panel anchored to a pill trigger — the Menu panel) and optical size (a surface too small for the container radius to read as intentional — the ChatMessage bubble, Textarea at tier="commit", Card tier="bridge") |
Motion & Depth
The interaction layer ships real --blocks-duration-* and --blocks-shadow-* variables (plus easing curves like --blocks-ease-gentle). Durations collapse to 1ms under prefers-reduced-motion; prefers-contrast: more widens the focus ring to 3px and darkens subtle borders to text colour; in print, shadows drop to none and hairlines promote
to a real grey rule.
| Duration token | Value |
|---|---|
| --blocks-duration-instant | 75ms |
| --blocks-duration-fast | 150ms |
| --blocks-duration-normal | 250ms |
| --blocks-duration-slow | 350ms |
| --blocks-duration-slower | 500ms |
| --blocks-duration-slowest | 750ms |
| Shadow token | Source |
|---|---|
| --blocks-shadow-xs | var(--blocks-shadow-scale-xs) |
| --blocks-shadow-sm | var(--blocks-shadow-scale-sm) |
| --blocks-shadow-base | var(--blocks-shadow-scale-base) |
| --blocks-shadow-md | var(--blocks-shadow-scale-md) |
| --blocks-shadow-lg | var(--blocks-shadow-scale-lg) |
| Easing token | Value |
|---|---|
| --blocks-ease-linear | linear |
| --blocks-ease-gentle | cubic-bezier(0.25, 0.1, 0.25, 1) |
| --blocks-ease-springy | cubic-bezier(0.68, -0.55, 0.265, 1.55) |
| --blocks-ease-confident | cubic-bezier(0.4, 0, 0.2, 1) |
| --blocks-ease-bounce | cubic-bezier(0.68, -0.6, 0.32, 1.6) |
| --blocks-ease-smooth | cubic-bezier(0.4, 0, 0.6, 1) |
| --blocks-ease-snappy | cubic-bezier(0.4, 0, 0.2, 1) |
| Override point | Default |
|---|---|
| --blocks-overlay-enter-duration | 200ms |
| --blocks-overlay-exit-duration | 180ms |
| --blocks-overlay-backdrop-enter-duration | 200ms |
| --blocks-overlay-backdrop-exit-duration | 180ms |
| --blocks-overlay-easing | cubic-bezier(0.83, 0, 0.17, 1) |
| --blocks-tooltip-duration | var(--blocks-duration-fast) |
| --blocks-tooltip-easing | var(--blocks-ease-confident) |
| --blocks-popover-duration | var(--blocks-duration-fast) |
| --blocks-popover-easing | var(--blocks-ease-confident) |
| --blocks-collapse-duration | var(--blocks-duration-normal) |
| --blocks-collapse-easing | var(--blocks-ease-smooth) |
| --blocks-focus-ring-width | 2px |
| --blocks-focus-ring-offset | 2px |
| --blocks-focus-ring-color | var(--color-primary) |
| --blocks-press-scale | 0.98 |
Theme-level chroma knobs
Two tokens let a theme match its chrome to its chassis without touching contrast. The four
coloured themes set both; see blocks/src/lib/style/themes/forest.css. The Neutral theme sets neither, by design: it inherits the library-default cool grey chrome
and leaves the shadow tint untouched.
| Token | Default | What it does |
|---|---|---|
| --blocks-shadow-tint | 0 0 0 | Re-tint so shadows match your chassis. Declare on :root, see below. |
| --neutral-chrome-hue | 240 | Hue of the neutral intent chrome (bg-neutral / text-neutral / neutral borders). Keeps the warm-neutral ramp lightness; only the hue moves, so contrast is untouched. |
:root, never in @theme. Both are raw partial values, not standalone tokens — --blocks-shadow-tint is an oklch L C H triplet without an alpha channel, spliced
into oklch(var(--blocks-shadow-tint) / 0.05). Putting either inside @theme gets you nothing. The shipped themes declare them in a :root block after their @theme for
exactly this reason. The tint also only bites at the root. It is read inside the value of
--blocks-shadow-*, which is declared on :root, so it is substituted there and every descendant inherits a
finished shadow — setting --blocks-shadow-tint on a scoped element moves nothing. A scope that
needs its own tint has to re-declare the five steps beside it.