Skip to content

Design Tokens

DelightStack uses CSS custom properties (design tokens) for all visual styling. Components reference these tokens internally, so customizing them changes the look of every component at once.

The token system has two layers:

  • Primitives — the raw OKLCH color ramp (--color-50--color-950) and a few internal scales, all derived from a single brand seed. These are an implementation detail; don’t reference them directly.
  • Semantic tokens — the public API documented below (--color-bg, --color-text-muted, --radius-md, …). Components reference only these. They’re defined in terms of the primitives, so changing the brand seed (or any semantic token) re-themes everything.

The fastest way to get the tokens is to import the package — it ships them all with light/dark values built in:

import '@delightstack/styles';

To re-theme, override the brand seed (or any individual semantic token) in your own CSS. --color-primary is the only required seed — --color-secondary (accent controls) and --color-neutral (the background/text ramp tint) derive from it automatically:

:root {
--color-primary: #2563eb; /* the only required seed — re-derives everything */
--color-neutral: #6b7280; /* optional: decouple the surface tint from the brand */
--color-action: light-dark(#2563eb, #3b82f6); /* or override a token outright */
--radius-md: 0.5rem;
}

Customize the seeds below and the whole docs site re-themes in place — every component demo on every page runs on the same tokens. Your choices persist in this browser (the palette button in the navbar opens the same editor), and the generated CSS is ready to paste into your own app.

Theme

One color themes everything — the rest of the palette derives from it. Pick a primary and the whole site re-themes live. Saved in this browser.

Secondary (accent controls) and Neutral (the background tint) derive from Primary until overridden. Error, Success, and Warning are fixed hues.

No overrides yet — the pickers show the current defaults.

TokenRole
--color-bgPage background
--color-bg-mutedRecessed areas — input wells, code blocks, control tracks
--color-bg-activeHover / selected surface
--color-bg-disabledDisabled element backgrounds
--color-surfaceElevated surfaces — cards, popovers, modals
TokenRole
--color-textPrimary text
--color-text-mutedSecondary / helper text
--color-text-activeActive / pressed text
--color-text-disabledDisabled text
TokenRole
--color-borderDefault borders and dividers
--color-border-activeFocused / active borders
--color-border-disabledDisabled borders

Define only the base color — the disabled, active, and text states are derived automatically with relative color syntax.

:root {
--color-action: light-dark(#005640, #34d399);
--color-action-active: /* derived */ ;
--color-action-disabled: /* derived */ ;
--color-action-text: /* tinted off-white on the action color */ ;
--color-action-text-active: /* derived */ ;
--color-action-text-disabled: /* derived */ ;
}

Same six tokens as action, seeded from --color-secondary: --color-accent, --color-accent-active, --color-accent-disabled, --color-accent-text, --color-accent-text-active, --color-accent-text-disabled.

--color-error and --color-success each carry a full state set (-active, -disabled, -text, -text-active, -text-disabled). --color-warning is a single color. All three are fixed hues — red, green, and amber carry meaning, so they don’t derive from the brand seed. There is no separate “info” color: informational UI (info callouts, toasts) uses the brand’s --color-action. Each feedback color also has a soft tinted background for callouts and alerts:

:root {
--color-error: light-dark(#ef6262, #b04343);
--color-success: light-dark(#00b7a1, #2c8f83);
--color-warning: light-dark(#e89c08, #e0a020);
/* Soft backgrounds, mixed over the page background */
--color-error-bg: /* … */ ;
--color-success-bg: /* … */ ;
--color-warning-bg: /* … */ ;
--color-action-bg: /* … (used by info callouts/toasts) */ ;
}
TokenRole
--color-backdropScrim behind modals / sheets
TokenValueTypical usage
--radius-none0Sharp corners
--radius-sm2pxInputs, small elements
--radius-md5pxButtons, default elements
--radius-lg10pxCards, panels
--radius-xl20pxLarge containers
--radius-2xl30pxModals, sheets
--radius-3xl60pxProminent / hero elements
--radius-full1e5pxPills, avatars
TokenValueComponents
--layer-base0Default content
--layer-dropdown100Select dropdowns, menus
--layer-sticky200Sticky headers
--layer-drawer300Drawer, BottomSheet
--layer-modal400Modal, Alert, lightbox
--layer-popover500Popover, CommandPalette
--layer-toast600Toast notifications
--layer-tooltip700Tooltips
--layer-maxForce-on-top escape hatch
:root {
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
--font-serif: 'Playfair Display', ui-serif, serif;
--font-mono: ui-monospace, SF Mono, Menlo, monospace;
}

The --text-* scale is the public typography API. (Internally it aliases a numbered --font-size-* scale that powers the component size prop — see Size System.)

TokenValue
--text-xs0.65rem
--text-sm0.815rem
--text-base1rem
--text-lg1.1rem
--text-xl1.25rem
--text-2xl1.5rem
--text-3xl2rem
--text-4xl2.5rem
--text-5xl3rem
--text-6xl3.5rem

Fluid variants (--text-fluid-0--text-fluid-3) scale with the viewport via clamp().

:root {
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
--tracking-tight: -0.05em;
--tracking-normal: 0;
--tracking-wide: 0.05em;
--tracking-wider: 0.15em;
}

A numeric scale from --space-1 (0.25rem) to --space-15 (30rem), plus fluid variants --space-fluid-1--space-fluid-5 that scale with the viewport.

In light mode, components use traditional drop shadows; in dark mode the shadows fade out (dark shadows are invisible on dark surfaces), so elevation reads through surface color and borders instead.

TokenUsage
--shadow-smSubtle lift
--shadow-mdButtons, cards
--shadow-lgPopovers, dropdowns
--shadow-xlModals
--shadow-2xlProminent overlays
:root {
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--duration-slower: 500ms;
--ease-default: cubic-bezier(0.76, 0, 0.24, 1);
--ease-in: cubic-bezier(0.5, 0, 0.75, 0);
--ease-out: cubic-bezier(0.33, 1, 0.68, 1);
--ease-in-out: cubic-bezier(0.76, 0, 0.24, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* playful overshoot */
}

Every component’s skeleton loading mode is driven by one shared set of tokens, so all skeletons in an app shimmer in the same color and rhythm (and can be re-themed in one place):

:root {
/* Placeholder fill — translucent text color so it sits on any surface */
--skeleton-bg: color-mix(in oklch, var(--color-text) 10%, transparent);
/* The brighter beam swept across each placeholder shape */
--skeleton-sheen: color-mix(in oklch, var(--color-text) 12%, transparent);
/* One shimmer cycle: the beam sweeps ~55% of the cycle, then rests */
--skeleton-duration: 2.4s;
}

The sweep itself is the global delight-skeleton-shimmer keyframe (each component declares it, so it works even without the global stylesheet). Components with several placeholder shapes stagger the sweep per item, which makes the shimmer travel through the skeleton as a wave.

Components use a numeric size prop where '1' is the default. The prop maps to font-size internally (via the numbered --font-size-* scale), and the rest of the component scales with em units:

'0000' → '000' → '00' → '0' → '1' (default) → '2' → '3' → '4' → '5' → '6'

Not every component uses the full range — each defines the relevant subset:

<Button size="0">Small</Button>
<Button>Normal (default)</Button>
<Button size="3">Large</Button>

Form controls (Input, Select, the form Button heights, and the smaller controls) use a '0''3' subset that also drives a shared height — see Control sizing.

Form controls share one canonical height system so a row of them lines up. The size prop picks a font from the --control-font-* scale, and the control’s height is then font × ratio. The density modifiers (dense / comfortable) swap the ratio. One scale → a default row is the same height, and a dense row is the same height, with no per-component hardcoding.

:root {
/* rem so controls scale with the root font-size; even-px fonts × 0.5-step
ratios keep every size on a whole-pixel height */
--control-font-0: 0.875rem; /* 14px → 42px tall */
--control-font-1: 1rem; /* 16px → 48px tall (default) */
--control-font-2: 1.125rem; /* 18px → 54px tall */
--control-font-3: 1.25rem; /* 20px → 60px tall */
--control-height-ratio: 3; /* default → 48px @ size 1 */
--control-height-ratio-dense: 2.5; /* dense → 40px @ size 1 */
--control-height-ratio-comfortable: 3.5; /* comfortable → 56px @ size 1 */
--control-pad-x: 1em; /* inline padding */
--control-pad-x-dense: 0.75em;
--control-pad-x-comfortable: 1.25em;
}

Input, Select and Button (including standalone icon buttons) snap to these heights:

sizedensedefaultcomfortable
035px42px49px
140px48px56px
245px54px63px
350px60px70px

So a row of controls at the same size (and density) is the same height:

<div style="display: flex; gap: 0.75rem; align-items: start;">
<Input label="Search" />
<Select label="Sort" {options} />
<Button>Go</Button>
</div>

The smaller controls (Checkbox, Radio, Toggle, Range, Rating) read the same --control-font-* scale so size means the same text scale everywhere; they keep their natural glyph height and center within a control-height row (align-items: center).

Used sparingly for page-level layout (components prefer container queries):

:root {
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
}