Design Tokens
A comprehensive design token system built with CSS custom properties and Tailwind 4. Organized in semantic layers for consistent theming and easy customization.
Token Architecture
Foundation Layer
Base design decisions like colors, spacing, and typography scales.
Raw ValuesSemantic Layer
Intent-based tokens that map foundation tokens to semantic meanings.
ContextualInteraction Layer
Motion, shadow, and focus tokens for consistent animations and depth.
Motion & DepthFoundation Colors
@theme {
/* Foundation Colors (oklch for better perception) */
--color-neutral-50: oklch(0.98 0.005 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(--color-shadow-sm);
--blocks-shadow-md: var(--color-shadow-md);Color System
Neutral Palette
The neutral ramp has 16 steps: alongside 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 500/600/700 stops are tuned dark enough that white text-on-fill passes WCAG AA on the solid fills — that 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).
A separate warm-neutral ramp powers the themeable neutral intent chrome.
primary
secondary
success
warning
danger
info
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
Type is themeable exactly like color. Sizes, weights, leading, tracking and font families are
Tailwind @theme variables — --text-sm, --font-weight-medium, --leading-tight, --tracking-wide, --font-sans, --font-mono — and
you override them in the same @theme block that
retunes --color-primary-500. This docs site is the proof: it rethemes the whole library's type by overriding --font-mono and --font-sans — see apps/docs/src/lib/style/rooms-docs.css.
- Change the paired line-height too. Tailwind's
built-in sizes each ship a companion
--text-*--line-height. Resize without it and the rhythm goes subtly wrong everywhere the size is used. The two library-added steps (--text-2xs,--text-3xs) are deliberately size-only — Tailwind emits aline-heightonly when the paired key exists, so they inherit it from the cascade. Add the paired key in your own@themeif you want one. - This works because of one property: the library
deliberately does not
@import 'tailwindcss', so there is exactly one Tailwind compilation — yours — and it wins. If your tooling introduces a second one, typography overrides silently revert, exactly like color overrides do. See docs/TailwindCaveats.md.
Theme the type scale
/* app.css — the SAME @theme block that retunes color.
Safe because the library never re-imports Tailwind: your
@theme is compiled last and wins. */
@import 'tailwindcss';
@import '@urbicon-ui/blocks/style/index.css';
@theme {
/* Families — blocks never sets `font-sans`, so body type simply
inherits from your page. It DOES use `font-mono` (CommandPalette
shortcut keys, JourneyTimeline meta), so this retunes those. */
--font-sans: 'Inter Variable', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
/* Size AND its paired line-height. Tailwind's built-in sizes each
ship a --text-*--line-height; changing the size alone leaves the
old rhythm behind on all ~128 text-sm call sites. */
--text-sm: 0.9375rem;
--text-sm--line-height: calc(1.375 / 0.9375);
--font-weight-medium: 550;
--leading-tight: 1.3;
--tracking-wide: 0.02em;
}Know where the leverage is. The library's usage is
steep and lopsided: --text-sm reaches
the most call sites by a wide margin, while nothing above text-xl is used at all — so
overriding --text-6xl changes nothing in the library, and --text-sm reshapes
it. And because blocks never sets font-sans, body
type inherits from your page: you already own the font decision — no override needed.
| Utility | Override | Value | Pixels | Uses in blocks | Example |
|---|---|---|---|---|---|
| text-3xs | --text-3xs | 0.625rem | 10px | 13 | The quick brown fox |
| text-2xs | --text-2xs | 0.6875rem | 11px | 18 | The quick brown fox |
| text-xs | --text-xs | 0.75rem | 12px | 118 | The quick brown fox |
| text-sm | --text-sm | 0.875rem | 14px | 163 | The quick brown fox |
| text-base | --text-base | 1rem | 16px | 91 | 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 | 5 |
| font-medium | --font-weight-medium | 500 | 66 |
| font-semibold | --font-weight-semibold | 600 | 51 |
| 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 — see the Tier System for the full cascade. 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 promotes hairline borders; in print, shadows drop to none.
| 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(--color-shadow-xs) |
| --blocks-shadow-sm | var(--color-shadow-sm) |
| --blocks-shadow-base | var(--color-shadow-base) |
| --blocks-shadow-md | var(--color-shadow-md) |
| --blocks-shadow-lg | var(--color-shadow-lg) |
| --blocks-shadow-tint | 0 0 0 · oklch L C H, no alpha |
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 | oklch L C H triplet (no alpha) spliced into every --color-shadow-*. Re-tint so shadows match your chassis instead of reading as cool smudges. |
| --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.Custom Theming
Use Tailwind 4's @theme directive to override tokens, or import one of the shipped themes — see Themes for the
full gallery.
Custom Theme Example
@import '@urbicon-ui/blocks/style/index.css';
/* Option A — import a shipped theme
(neutral, ocean, forest, rose, sunset) */
@import '@urbicon-ui/blocks/style/themes/ocean.css';
/* Option B — re-tint the ramps yourself. The semantic layer
consumes several stops (600/500 base, 700/400 hover,
800/300 active, 900/200 emphasis, 50/900 subtle), so
override the WHOLE ramp: keep each stop's lightness and
chroma, swap only the hue. */
@theme {
--color-primary-500: oklch(0.58 0.15 280);
--color-primary-600: oklch(0.52 0.15 280);
--color-primary-700: oklch(0.44 0.13 280);
/* … all stops 50–950 with the new hue */
/* The chassis, NOT optional: surface/text/border derive
from neutral, so a purple brand on the default cool
240 chassis reads broken. Same L/C, hue → 290. */
--color-neutral-50: oklch(0.98 0.005 290);
--color-neutral-500: oklch(0.55 0.016 290);
--color-neutral-900: oklch(0.15 0.012 290);
/* … all 15 stops (25–950) with the new hue. Leave
--color-neutral-0 (pure white) alone — tinting it
tints your white. */
}
/* Raw partial values — :root, never @theme (see Motion & Depth). */
:root {
--blocks-shadow-tint: 0.2 0.025 290;
--neutral-chrome-hue: 290;
}
/* Custom brand tokens get Tailwind utilities for free
(bg-brand-500, text-brand-500, …) */
@theme {
--color-brand-500: oklch(0.5 0.25 45);
}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. Use the override ladder:
class→slotClasses→BlocksProviderpresets/overrides →unstyled. Never force colors with!overrides — see BlocksProvider.
Dark Mode Support
Design tokens automatically adapt to dark mode using light-dark(): :root declares color-scheme: light dark, so the browser resolves the matching branch from the user's preference. A manual toggle
only sets :root.light / :root.dark to override
the color-scheme — no
token duplication, and no manual dark: overrides needed.