Skip to main content

Themes

Pick a look below, or from the picker in the navbar, and this whole site changes with it: colours, type, spacing, control sizes, corners, shadows and motion. Each theme is one block of CSS that overrides the ZUI tokens, applied to <html>. The selected block is printed beside the cards, so what you read is exactly what is running. Every theme has a light and a dark mode; the toggle next to the picker flips between them.

Two of the looks are ours. The other four are products you already use, rebuilt from each product’s published palette and metrics, so you can see your components under the app they will live in before writing a line of theme CSS. Every shipped theme passes zui theme check with zero errors, so each one is a working example of an AA-clean override file.

Spec sheet

The site’s own look. Green ink on warm yellow paper with a red accent, every radius 0 — pill badges and buttons included — no shadows, quick motion, a regular type scale and a paper sidebar. Dark mode inverts the paper to a deep green and lifts the ink to cream.

theme-spec.css
.theme-spec {
/* Type */
--font-sans: "Instrument Sans", "Avenir Next", "Segoe UI", sans-serif;
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-base-line-height: 1.6;
--text-lg: 1.125rem;
--text-xl: 1.35rem;
--text-2xl: 2rem;
--text-3xl: 2.75rem;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 600;

/* Density */
--spacing: 0.25rem;
--control-height-sm: 2rem;
--control-height-md: 2.5rem;
--control-height-lg: 2.75rem;
--control-padding-x-sm: 0.6rem;
--control-padding-x-md: 0.75rem;
--control-padding-x-lg: 1rem;
--button-padding-x-sm: 0.75rem;
--button-padding-x-md: 1rem;
--button-padding-x-lg: 1.5rem;

/* Shape and depth */
--radius: 0;
--radius-sm: 0;
--radius-md: 0;
--radius-lg: 0;
--radius-xl: 0;
--radius-button: 0;
--shadow-1: none;
--shadow-2: none;
--shadow-3: none;
--shadow-card: none;
--shadow-4: 0 0 0 1px rgba(11, 51, 36, 0.18);
--shadow-glow: none;

/* Motion */
--duration-fast: 100ms;
--duration-normal: 150ms;
--duration-slow: 220ms;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-decelerate: cubic-bezier(0, 0, 0.2, 1);

/* Colour */
--background: #fbf7e9;
--foreground: #0b3324;
--card: #fffdf4;
--card-foreground: #0b3324;
--popover: #fffdf4;
--popover-foreground: #0b3324;
--border: rgba(11, 51, 36, 0.18);
--input: rgba(11, 51, 36, 0.3);
--ring: #008751;
--muted: #f3ecd1;
--muted-foreground: #485a4f;
--primary: #007a48;
--primary-foreground: #fbf7e9;
--primary-text: #006b40;
--secondary: #f3ecd1;
--secondary-foreground: #0b3324;
--accent: #fcd116;
--accent-foreground: #0b3324;
--danger: #d8102a;
--danger-foreground: #fbf7e9;
--success: #008751;
--success-foreground: #ffffff;
--warning: #fcd116;
--warning-foreground: #0b3324;
--gradient-primary: linear-gradient(90deg, #008751 0 50%, #fcd116 50% 100%);
--gradient-hero: linear-gradient(90deg, #008751 0 33%, #fcd116 33% 66%, #e8112d 66% 100%);
--gradient-subtle: linear-gradient(180deg, #fbf7e9, #f3ecd1);
--sidebar: #fbf7e9;
--sidebar-foreground: #0b3324;
--sidebar-primary: #007a48;
--sidebar-primary-foreground: #fbf7e9;
--sidebar-accent: #f3ecd1;
--sidebar-accent-foreground: #0b3324;
--sidebar-border: rgba(11, 51, 36, 0.18);
}

.dark .theme-spec {
--background: #06211a;
--foreground: #f5e9bd;
--card: #0d3127;
--card-foreground: #f5e9bd;
--popover: #0d3127;
--popover-foreground: #f5e9bd;
--border: rgba(245, 233, 189, 0.18);
--input: rgba(245, 233, 189, 0.3);
--ring: #2fd07f;
--muted: #0d3127;
--muted-foreground: #9db5a2;
--primary: #2fd07f;
--primary-foreground: #06211a;
--primary-text: #6fe3a6;
--secondary: #0d3127;
--secondary-foreground: #f5e9bd;
--accent: #184030;
--accent-foreground: #f5e9bd;
--danger: #ff5468;
--danger-foreground: #06211a;
--success: #2fd07f;
--success-foreground: #06211a;
--shadow-4: 0 0 0 1px rgba(245, 233, 189, 0.18);
--gradient-subtle: linear-gradient(180deg, #06211a, #0d3127);
--sidebar: #06211a;
--sidebar-foreground: #f5e9bd;
--sidebar-primary: #2fd07f;
--sidebar-primary-foreground: #06211a;
--sidebar-accent: #0d3127;
--sidebar-accent-foreground: #f5e9bd;
--sidebar-border: rgba(245, 233, 189, 0.18);
}

How a theme works​

Components read token names (--primary, --radius, --spacing); a theme gives those names values. Redeclare the values and every component follows. A theme is a selector plus a set of declarations:

.theme-teal {
--primary: #0f766e;
--primary-foreground: #ffffff;
--radius: 0.25rem;
--font-sans: "Inter", system-ui, sans-serif;
--control-height-md: 2.25rem;
}

.dark .theme-teal {
--primary: #2dd4bf;
--primary-foreground: #042f2e;
}

Where you put the selector decides its reach. On :root or <html> it themes the whole app; on a class it themes that subtree, which is how a panel, a preview or an embedded editor can carry its own brand inside a host (component theming shows this). Dark mode is the same mechanism: a second set of values under a .dark ancestor, repeating only what changes. Anything a theme leaves out keeps the shipped default, so a theme that changes three colours is three lines.

Two rules keep every theme readable:

  1. Name the role. --primary is the product's principal colour, whatever it is: blue for Foundry, orange for HubSpot, purple for Stripe.
  2. Foregrounds travel with their backgrounds. --primary-foreground exists so a theme can make --primary as dark or as pale as it likes and the text on it stays legible.

What the themes change​

AxisTokensAcross the six themes
ColourSemantic pairs, surfaces, gradients, sidebarSpec sheet is green ink on yellow paper; HubSpot pairs an orange action with a navy chrome; Stripe leans on a purple ring; Foundry sets Blueprint's intent blue against a dark platform rail
Type--font-sans, --font-mono, --text-*, --font-weight-*Microsoft 365, Stripe and Foundry run at 14px; Spec sheet keeps a full 16px scale
Spacing--spacingOne number. Every gap, padding and table cell derives from it, so lifting it a quarter makes the whole app breathe
Density--control-height-*, --control-padding-x-*, --button-padding-x-*A medium control is 1.75rem in Stripe, 1.875rem in Foundry, 2rem in Microsoft 365, 2.5rem in HubSpot
Shape--radius, --radius-sm … --radius-xlSpec sheet sets every radius to 0; Foundry stops at 2px, HubSpot at 3px, Microsoft 365 at 4px. --radius is what every rounded-* utility derives from, with the steps for plain-CSS readers
Depth--shadow-1 … --shadow-4, --shadow-card, --shadow-glownone for Spec sheet; Stripe and Foundry layer a ring under a drop; Microsoft 365 uses the Fluent ramp
Motion--duration-*, --ease-standard, --ease-decelerateFluent's easy-ease for Microsoft 365, Blueprint's quick 100ms ease for Foundry

The structure of the product stays the same across all of them. A new visual identity costs a few dozen lines of CSS. Start from theme-template.css, which lists every token with its default, or npx @zuilib/cli theme init, and run npx @zuilib/cli theme check brand.css before merging: it catches typos, values that do not parse, colours set in one mode only, text, tint, cross-surface and border pairs below their WCAG bar and overrides that change nothing, so a theme fails a CI build rather than a design review; zui theme diff old.css new.css prints what a revision changed (theme validation).