Theming
The Layout UI token contract: how --layout-* canonical tokens, shadcn compatibility, dark mode, brand scoping, and density all fit together.
The token contract
Layout UI defines a canonical --layout-* token namespace. Components never reference raw values or arbitrary colours. They always reference intent tokens like --layout-primary or --layout-border. Because no component hard-codes a value, changing a token changes every surface that consumes it.
| Token | Tailwind utility resolves to | Purpose |
|---|---|---|
| --layout-bg | bg-background | Page background |
| --layout-fg | text-foreground | Primary body text |
| --layout-surface | bg-card | Raised surface (cards) |
| --layout-overlay | bg-popover | Floating layers |
| --layout-primary | bg-primary / text-primary | Primary actions, CTAs |
| --layout-primary-fg | text-primary-foreground | Text on primary |
| --layout-secondary | bg-secondary | Secondary actions |
| --layout-muted | bg-muted | Subdued fills |
| --layout-muted-fg | text-muted-foreground | Secondary text |
| --layout-accent | bg-accent | Hover fills, tonal containers |
| --layout-danger | bg-destructive | Destructive / error state |
| --layout-success | bg-success | Success state |
| --layout-warning | bg-warning | Warning state |
| --layout-border | border-border | All borders and dividers |
| --layout-input | border-input | Form field borders |
| --layout-ring | ring, outline-color | Focus ring colour |
| --layout-radius | --radius-lg (base) | Base border-radius; all variants derive from this |
| --layout-shadow-sm | shadow-sm | Card-level elevation |
| --layout-shadow-md | shadow-md | Dropdown elevation |
| --layout-duration-fast | duration-[var(--layout-duration-fast)] | Quick micro-interactions (100ms) |
| --layout-duration-base | duration-[var(--layout-duration-base)] | Standard transitions (150ms) |
| --layout-ease-out | ease-out | Deceleration easing |
| --layout-space-unit | --spacing (Tailwind base) | All spacing utilities scale from this unit |
shadcn compatibility
Every Tailwind utility in Layout UI resolves through a fallback chain. For example, bg-background maps to:
--color-background: var(--background, var(--layout-bg));If a shadcn or tweakcn theme sets --background, that value wins. Without one, the --layout-bg default drives the colour. You can drop Layout UI components into any shadcn project without any token conflict. The host theme simply takes precedence.
Dark mode
Dark mode is applied by adding .dark or data-theme="dark" to the <html> element. The Layout token contract ships dark values for every token out of the box. Use the ThemeToggle component from this site as a reference implementation.
/* globals.css, auto-included with @layout/theme-layout */
@custom-variant dark (
&:where(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *)
);
[data-theme="dark"] {
--layout-bg: oklch(0.17 0.004 95);
--layout-primary: oklch(0.93 0.004 95);
/* ... all tokens redefined for dark ... */
}Brands and scoped reskinning
A brand is simply a CSS block that redefines --layout-* tokens under a [data-brand] selector. Set the attribute on <html> to reskin the entire app, or on any wrapper element to scope the reskin to that subtree.
The three demos below are rendered on the server with scoped data-brand wrappers: the same Button, Card, Badge, and Input components, zero changes:
data-brand="stripe" · Stripe kit
data-brand="linear" · Linear kit
data-brand="notion" · Notion kit
/* How a brand is defined in themes.css */
[data-brand="stripe"] {
--layout-bg: #f6f9fc;
--layout-primary: #635bff;
--layout-primary-fg: #ffffff;
--layout-radius: 0.375rem;
--layout-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* ... all tokens ... */
}Layout kits from layout.design are compiled from design tokens and style profiles extracted from real products. Install a kit and apply its brand attribute to instantly reskin your project.
Every kit in the gallery is available in the brand switcher in the top bar: the entire gallery compiles into theme blocks automatically. In this repo, npm run sync:kits fetches all published kits from the layout.design API, maps each kit's style profile (16 brand colours, radii, shadows, density, mode) onto the token contract, and regenerates kit-themes.css plus the brand manifest. A new kit published to the gallery becomes a theme by re-running the sync; no component work, ever.
Density
Density is controlled by a single CSS custom property, --layout-space-unit, which maps to Tailwind's --spacing base unit. Every spacing utility (p-4, gap-2, h-9) scales proportionally, with no per-component overrides needed.
/* Comfortable (default) */
:where(:root) {
--layout-space-unit: 0.25rem; /* 1 spacing unit = 4px */
}
/* Compact */
[data-density="compact"] {
--layout-space-unit: 0.215rem; /* ~14% tighter across all UI */
}Apply compact density by setting data-density="compact" on <html>. The DensityToggle in this site's top bar is a working example.