Skip to content

Theme

The @diffgazer/ui two-layer CSS variable system — primitives, semantic tokens, cascade layers, surface elevation, z-index, color, typography, and dark mode.

@diffgazer/ui themes through a two-layer CSS variable system. Primitives (--base-bg, --base-fg, --base-info, …) define the raw palette. Semantic tokens (--background, --foreground, --primary, …) reference those primitives for functional meaning, and every component reads only the semantic layer.

Override the primitives to reskin every component at once — subtle tone backgrounds and component-scoped tokens both derive from the primitives, so a single override re-tints everything. Override semantic tokens for fine-grained control. Component-scoped tokens (e.g. --panel-hairline) are consumed with var() fallbacks, so an ancestor-scoped override now reaches them while a chosen variant's own value still wins.

Two semantic roles are easy to confuse because they coincide in dark: --primary is monochrome emphasis, --action is the call-to-action pair and the one role allowed to carry chroma. See Action vs primary before repointing either.

Variable architecture

--base-bg feeds --background; --base-fg feeds --foreground; --base-fg feeds --primary; --base-fg feeds --ring; --base-info feeds --info; --base-success feeds --success; --base-danger feeds --error; --base-warning feeds --warning; --base-border feeds --border; --base-dim feeds --border-strong; --base-muted feeds --muted; --base-highlight feeds --accent; --base-highlight feeds --action; --base-selection feeds --secondary; --base-selection feeds --input; --base-selection feeds --card; --base-selection feeds --popover

Primitives (dark)
--base-bg#0a0a0a / #f7f8f5
--base-fg#e5e5e5 / #1f2328
--base-info#79b8ff / #0b63ce
--base-success#7ee787 / #0f7a4f
--base-danger#ff7b72 / #c62828
--base-warning#d29922 / #8a5a00
--base-accent#808080 / #6f42c1
--base-border#606060 / #aeb7c0
--base-dim#9c9c9c / #5f6872
--base-muted#808080 / #69717a
--base-highlight#ffffff / #1f2328
--base-selection#333333 / #e8edf3
--base-input-bg#0a0a0a / #ffffff
Semantic Tokens
--background
--foreground
--primary
--ring
--info
--success
--error
--warning
--border
--border-strong
--muted
--accent
--action
--secondary
--input
--card
--popover

Theme playground

Primitives

--base-bg
--base-fg
--base-dim
--base-info
--base-accent
--base-success
--base-danger
--base-warning
--base-border
--base-highlight
--base-highlight-foreground
--base-selection
--base-muted
--base-input-bg

Preview

infosuccesswarningerror
Info:
This is an informational callout.
Warning:
This is a warning callout.

Panel Title

Panel content with theme variables applied.

Generated CSS

No changes yet. Edit a color above to generate CSS.

Cascade layers

@diffgazer/ui/styles/theme-base.css declares the cascade layer order on its first non-comment line:

css
@layer theme, base, components, utilities;

The order lets consumers predict override resolution without depending on bundler source order or import sequence. Layers later in the list win against layers earlier in the list, so:

  • theme holds design tokens (--color-*, --radius-*, --z-*, --animate-*).
  • base holds element resets and global typography.
  • components holds component class styles.
  • utilities holds Tailwind-generated utility classes and authored utilities.

Tailwind v4 emits its own utilities and components into the matching layer names, so the same precedence applies to the classes you write in JSX.

When you add custom CSS, place it inside the appropriate @layer block to opt into the documented precedence. Unlayered CSS still loads, but it always wins over any @layer rule and therefore breaks the contract — keep that for one-off overrides you want to "always apply."

Surface elevation

Surface tokens describe stepwise elevation above the page background (--base-bg). They are declared on :root and the [data-theme="light"] override block in theme.css.

TokenDark defaultLight defaultUsed by
--surface-1#111111#eef0f3Panel frame="surface" for Linear/Raycast-style elevated card surfaces
--surface-1-highlightcolor-mix(in oklab, var(--base-fg) 12%, transparent)#ffffffThe 1px inner top lip drawn on --surface-1 fills
--surface-2#181818#e7eaeeDialogHeader's strip band, and the Panel.Label / CardLabel corner chip in variant="border"

--surface-1 is one step off --base-bg: lighter in dark, and darker in light, because an off-white page has no visible step left going lighter. --surface-1-highlight is the 1px inner top edge that keeps the step reading as raised in both directions. --surface-2 is the next step in the same direction, for chrome that has to sit above an already-elevated surface — the dialog header band and the chips seated on a panel or card border. Override them on :root (or any ancestor) to retint every elevated panel without forking the component; repoint --surface-2 alongside --surface-1 so the two steps keep reading as one ramp:

css
:root {
  --surface-1: #161616;
  --surface-2: #1e1e1e;
}

Hard shadow

Flat surfaces get their depth from a border plus --surface-1, not from blur. The one sanctioned shadow is --shadow-hard, a hard offset with zero blur, reserved for the modal overlay tierDialog and CommandPalette. Anchored overlays (Popover, Tooltip, Select content and variant="card", Menu submenus) carry the surface step and hairline alone; see Overlay elevation.

Both themes mix the offset from --base-fg at the same percentage, so the shadow reads identically on either background. A near-black offset over a near-black page composites to ~1.05:1 and disappears, which is why dark does not use pure black.

TokenDark defaultLight default
--shadow-hard4px 4px 0 0 color-mix(in oklab, var(--base-fg) 22%, transparent)4px 4px 0 0 color-mix(in oklab, var(--base-fg) 22%, transparent)

It has no Tailwind bridge (--shadow-* is Tailwind's own theme namespace), so consume it directly:

tsx
<div className="shadow-(--shadow-hard)"></div>
css
.my-floating-surface {
  box-shadow: var(--shadow-hard);
}

Overlay elevation

Every overlay in the library shares one fill and differs only in whether it also carries the slab shadow. OVERLAY_SURFACE and OVERLAY_SURFACE_MODAL (registry/ui/shared/overlay-surface.ts) are the two class recipes; nothing else should re-spell them.

TierSurfacesRecipe
AnchoredPopover, Tooltip, Select.Content, Select variant="card", Menu.SubContent--surface-1 fill + 1px --border hairline + --surface-1-highlight inner lip
ModalDialog, CommandPalettethe anchored fill and lip plus --shadow-hard; the hairline comes from each surface's own frame axis

The fill step is what carries the elevation when the offset shadow is clipped — at 375/390 the shadow falls off the viewport edge, and the panel still reads as a raised plane because it is a step off the page in both themes.

Z-index scale

Five tokens cover every overlay surface in the library. They live in the @theme block of theme-base.css and are exposed as CSS variables:

TokenValueUsed by
--z-base1Panel.Label / CardLabel corner chip (in-flow chrome that must paint above neighboring siblings)
--z-dropdown100Reserved for future inline menu/select surfaces
--z-overlay200Reserved for full-viewport dim/scrim overlays
--z-popover300Popover, Tooltip, Select content
--z-toast400Toaster

Consumers apply a token through Tailwind's arbitrary value syntax:

tsx
<div className="fixed z-[var(--z-popover)]"></div>

Override a token in your own theme by redeclaring it on :root (or any ancestor) to bump a specific surface without forking the component:

css
:root {
  --z-toast: 500;
}

Scrim

Modal backdrops share one token, --scrim (default rgb(0 0 0 / 0.6) in both themes), declared on :root and the [data-theme="light"] block. The native Dialog backdrop consumes it (dialog::backdrop { background: var(--scrim) }), and overlay surfaces in consuming apps can reuse the same value. Override it once to retint every modal dim layer:

css
:root {
  --scrim: rgb(0 0 0 / 0.75);
}

Native top-layer caveat

Native <dialog>.showModal() and the HTML Popover API render in the browser's top layer, which sits above every painted z-index value. The z-index scale does not control whether a Toast paints over a native modal Dialog; the Dialog's top-layer placement does.

Concretely:

  • A toast with --z-toast: 400 is not visible above an open <dialog> opened with .showModal() unless the toast itself is promoted to the top layer.
  • Two top-layer surfaces stack in open order — the most recently opened one wins, regardless of z-index.
  • Increasing --z-toast to an arbitrarily large number does not change this behavior.

Use the z-index scale to order surfaces that all sit in the normal stacking context. Use the top-layer behavior intentionally when you need an element guaranteed to paint above everything else on the page.

Floating surface animation tokens

FloatingPanel (and primitives that compose it — Popover, Select, future Menu/Combobox) reads these tokens to drive enter/exit animations. They are declared in @theme in theme-base.css and the .ui-floating-panel rule consumes them under the @layer components cascade.

TokenDefaultUsed by
--ui-floating-zvar(--z-popover)z-index of .ui-floating-panel.
--ui-content-enter-duration60msShared duration for every directional enter token.
--ui-content-exit-duration40msShared duration for every directional exit token.
--ui-content-enter-from-{top|bottom|left|right}ui-content-enter-fade var(--ui-content-enter-duration) linearanimation shorthand applied while [data-state="open"].
--ui-content-exit-to-{top|bottom|left|right}ui-content-exit-fade var(--ui-content-exit-duration) linear forwardsanimation shorthand applied while [data-state="closed"].
--ui-content-transform-originset by FloatingPanelResolved from data-side/data-align. Read it from transform-based keyframe overrides.
--ui-floating-trigger-widthset by FloatingPanel when matchTriggerWidthWidth of the trigger, in pixels. Use as width, min-width, or max-width on the panel.

Under prefers-reduced-motion: reduce, all eight directional tokens collapse to the fade-only keyframes. Opacity transitions still run; directional motion does not. Override per instance to opt back in.

See the FloatingPanel page for override examples.

In this section

PageCovers
ColorsThe full palette as a copy-to-clipboard swatch grid mapped to CSS variable names.
TypographyJetBrains Mono font stack, the fixed (non-fluid) size scale, and the two weights.
Dark Mode[data-theme] switching, the [data-theme="light"] override block, persistence, and flash prevention.
DiffgazerThe Diffgazer theme — monochrome primitives, per-variant token mapping, and data-theme variants.