Theme
Theme tokens for base, overlay, brand, and status. Light and dark modes attached to the variable, not the layer.
Theme is where modes live. It is the surface layer every component consumes, and the only layer that swaps values when the mode switches.
Theme tokens describe roles: --background, --card, --primary, --destructive. They resolve to palette values based on the active mode. No component reaches past theme to grab a palette value directly. Change --primary at the theme layer, and every primary surface in the product follows.
Theme follows shadcn/ui's background-foreground pattern, with a few departures noted at the bottom of this page.
Base
Base tokens cover the bedrock surfaces and the text that sits on them.
| Token | Light | Dark | Role |
|---|---|---|---|
--background | --neutral-50 | --neutral-950 | The page surface everything else sits on. |
--foreground | --neutral-900 | --white | Default text. |
--border | --neutral-200 | --neutral-800 | Default border lines. |
--border-foreground | --tint-950 | --tint-100 | Text color when it sits on a border (outlined buttons, ghost inputs, bordered tags). |
--card | --white | --neutral-900 | Card surfaces, one step elevated from background. |
--card-foreground | --neutral-900 | --white | Text on cards. |
--input | --neutral-50 | --tint-50 | Input field background. |
--input-foreground | --neutral-900 | --white | Input text. |
.card {
background: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
border-radius: var(--radius-card);
}Overlay
Overlay tokens cover dimming, fading, and the muted text that goes with them.
| Token | Light | Dark | Role |
|---|---|---|---|
--muted | --shade-50 | --tint-50 | A subtle dim over whatever sits below. |
--muted-foreground | --shade-600 | --tint-600 | Text on muted surfaces. Also used for de-emphasized labels and helper copy. |
--faded | --tint-100 | --shade-100 | A subtle fade over whatever sits below. |
--faded-foreground | --tint-600 | --shade-600 | Text on faded surfaces. |
Overlays swap their underlying primitive between modes: --muted uses shade-* in light and tint-* in dark; --faded does the opposite. The semantic stays constant. Muted is always darker than the surface, faded is always lighter, even though the underlying token shifts.
.section-divider {
background: var(--muted);
color: var(--muted-foreground);
}Brand
Brand tokens map the active mode to a step on each corporate scale.
| Token | Light | Dark | Role |
|---|---|---|---|
--primary | --primary-600 | --primary-500 | Primary brand. |
--primary-foreground | --white | --white | Text on primary surfaces. |
--secondary | --secondary-600 | --secondary-500 | Secondary brand. |
--secondary-foreground | --white | --white | Text on secondary surfaces. |
--tertiary | --tertiary-500 | --tertiary-400 | Tertiary brand. |
--tertiary-foreground | --white | --white | Text on tertiary surfaces. |
Brand colors step lighter in dark mode (600 → 500, 500 → 400) so they stay readable against the darker background. Tertiary takes a slightly lighter step than primary and secondary. Most tertiary brand colors carry more chroma per step, so a lighter base reads cleaner.
Re-target the scale steps at the theme layer to rebrand without touching components. See Corporate for the full scales.
Status
Status tokens cover info, success, warning, and destructive.
| Token | Light | Dark | Role |
|---|---|---|---|
--info | oklch(0.5876 0.1389 241.97) | oklch(0.5 0.1193 242.75) | Informational, neutral notices. |
--success | oklch(0.596 0.1274 163.23) | oklch(0.5081 0.1049 165.61) | Successful outcomes, positive confirmations. |
--warning | oklch(0.6658 0.1574 58.32) | oklch(0.5553 0.1455 49) | Cautionary notices, soft alerts. |
--destructive | oklch(0.5771 0.2152 27.33) | oklch(0.5054 0.1905 27.52) | Errors, irreversible actions. |
--*-foreground | --white | --white | Text on status surfaces. |
Status colors step darker in dark mode (lightness drops by roughly 0.08–0.11), opposite to brand. The reasoning: a status color is a signal, not an identity. Lightening a destructive red in dark mode makes errors the brightest thing on the page. Darkening keeps the signal present without overshouting the rest of the UI.
Status values are written in raw OKLCH so they sit in the same color space as every other Zazz token. Lightness, chroma, and hue can be tuned independently to hit a contrast target in either mode.
Two reasons.
Contrast stays consistent. A neutral-50 background with a Sky-600 info color and a neutral-950 background with a Sky-700 info color sit at roughly the same contrast ratio. The eye sees comparable separation in both modes, so the signal stays the same strength.
Hierarchy stays calm. In a dark UI, the brightest element commands attention. If destructive lightens into a vivid red while the rest of the surface stays muted, errors become the loudest thing on the page, even when nothing is wrong. Darkening keeps status present and distinct without making it the loudest voice.
If you want a brighter status for a specific product, the override is one variable away. Re-point --destructive (or any status token) at a different scale step at the theme layer.
How modes work
Zazz responds to both system preference and an explicit class.
:root {
/* light by default */
}
@media (prefers-color-scheme: dark) {
:root {
/* dark when the OS asks for it */
}
}
:is(.dark, .dark *) {
/* dark whenever .dark is on an ancestor */
}Force a mode by adding .dark (or any class you wire up) anywhere up the tree: for an in-page theme switcher, a preview panel, or a section that overrides the system default.
To add a new mode (high-contrast, brand-takeover, holiday), repeat the pattern: a new selector that re-points the same theme variables. Components never branch on mode; they consume theme tokens and trust them.
Beyond Tailwind and shadcn
The theme layer is compatible with Tailwind and shadcn and borrows the parts that work: background-foreground naming, color-scale conventions, the .dark ancestor pattern. Where Zazz makes different choices, the framework is geared more toward marketing and ecommerce sites than toward web apps. Both work; the defaults are tuned for the former.
The choices worth knowing:
- A text-on-border role exists.
--border-foregroundcolors text that sits on a border (outlined buttons, ghost inputs, bordered tags) without reaching into a palette directly. - Overlays split into muted and faded. Muted dims, faded fades. The split matters when stacking on product photography, building hero overlays, or designing dark-mode surfaces that need a different scrim than the light equivalent.
- Tertiary brand is a first-class role. Marketing and ecommerce clients often have three identity colors: a primary, an accent, and a "callout" hue used on sale tags, badges, or seasonal moments.
- Four status roles ship out of the box. Info, success, warning, and destructive in a consistent foreground-paired shape, so checkout flows and form validation work without bespoke wiring.
- Status darkens in dark mode. Opposite of brand. Alerts should not be the loudest thing on a marketing surface that's already loud with imagery.