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 six 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.

Spec sheet

The site’s own look. Green ink on warm yellow paper with a red accent, every radius 0, 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;
--shadow-soft: none;
--shadow-medium: none;
--shadow-large: none;
--shadow-card: none;
--shadow-elevated: 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-out: 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: #5a6f62;
--primary: #008751;
--primary-foreground: #fbf7e9;
--primary-text: #006b40;
--secondary: #f3ecd1;
--secondary-foreground: #0b3324;
--accent: #fcd116;
--accent-foreground: #0b3324;
--destructive: #e8112d;
--destructive-foreground: #fbf7e9;
--success: #008751;
--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-background: #fbf7e9;
--sidebar-foreground: #0b3324;
--sidebar-primary: #008751;
--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: #fcd116;
--accent-foreground: #06211a;
--destructive: #ff5468;
--destructive-foreground: #06211a;
--success: #2fd07f;
--shadow-elevated: 0 0 0 1px rgba(245, 233, 189, 0.18);
--gradient-subtle: linear-gradient(180deg, #06211a, #0d3127);
--sidebar-background: #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: pink for Bubble, 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 five 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; Bubble is pastel pink
Type--font-sans, --font-mono, --text-*, --font-weight-*Microsoft 365 and Stripe run at 14px; Bubble rounds its face and lifts the line height
Spacing--spacingOne number. Bubble sets it a quarter larger and every gap, padding and table cell breathes
Density--control-height-*, --control-padding-x-*, --button-padding-x-*A medium control is 1.75rem in Stripe, 2rem in Microsoft 365, 2.5rem in HubSpot
Shape--radius, --radius-sm--radius-xlSpec sheet sets every radius to 0; HubSpot stops at 3px, Microsoft 365 at 4px; Bubble takes controls to full pills
Depth--shadow-soft--shadow-elevated, --shadow-glownone for Spec sheet; Stripe layers a ring under a drop; Microsoft 365 uses the Fluent ramp; Bubble glows in its own tint
Motion--duration-*, --ease-standard, --ease-outFluent's easy-ease for Microsoft 365, slow with overshoot for Bubble

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.