Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Colors

The semantic OKLCH color tokens — light and dark, with a one-file override.

Last updated

Every color is a semantic token in OKLCH, defined in @vegastack/design-tokens/theme.css as a --<name> CSS variable on :root (light) and .dark. Components only ever use semantic utilities (bg-primary, text-muted-foreground), never raw palettes or hex.

The surface ladder

Neutral surfaces are one ladder, and every interaction step is a rung on it. Three rungs sit above the page; each is one even step (~0.025 L in light, ~0.03 L in dark).

LIGHT

--backgroundpage
--cardsurface · popover · sidebar
--surface-1rest fill · sunken well
--surface-2hover
--surface-3pressed · selected

DARK

--backgroundpage
--cardsurface · popover · sidebar
--surface-1rest fill · sunken well
--surface-2hover
--surface-3pressed · selected

ALPHA TWINS — LIGHT

on --background (page)

rest
hover · foreground/(--alpha-hover)
--surface-2
pressed · foreground/(--alpha-pressed)
--surface-3

on --card (surface)

rest
hover · foreground/(--alpha-hover)
--surface-2
pressed · foreground/(--alpha-pressed)
--surface-3

on --surface-1 (well)

rest
hover · foreground/(--alpha-hover)
--surface-2
pressed · foreground/(--alpha-pressed)
--surface-3

Each wash is the real foreground composite the recipes emit, painted over the host beside its opaque rung. On the page they land within 0.003 L of the rung; over the well and the dark card they keep stepping, which the opaque rung cannot do.

ALPHA TWINS — DARK

on --background (page)

rest
hover · foreground/(--alpha-hover)
--surface-2
pressed · foreground/(--alpha-pressed)
--surface-3

on --card (surface)

rest
hover · foreground/(--alpha-hover)
--surface-2
pressed · foreground/(--alpha-pressed)
--surface-3

on --surface-1 (well)

rest
hover · foreground/(--alpha-hover)
--surface-2
pressed · foreground/(--alpha-pressed)
--surface-3

Each wash is the real foreground composite the recipes emit, painted over the host beside its opaque rung. On the page they land within 0.003 L of the rung; over the well and the dark card they keep stepping, which the opaque rung cannot do.

TokenRole
--backgroundthe page
--cardevery surface — card, popover, sheet, dialog, the sidebar rail
--surface-1the rest fill of a filled control (soft button, kbd, chip, tab rail) and wells
--surface-2hover
--surface-3pressed / selected

Light surfaces are page-coloured — a light card is separated by its hairline alone. Dark keeps the one-step lift (0.175 → 0.205). --popover and --sidebar are --card; a floating surface is the card surface plus --shadow-overlay, never a rung of its own.

Each interaction rung also exists as an alpha twin--alpha-hover (7%) and --alpha-pressed (10%) on an ink. Over the page they land within 0.003 L of the opaque rungs, so reach for the alpha form when the backdrop is not a ladder surface (a Kbd inside a hovered row, a chip on a well, chrome over video) or when a control hovers in its own hue.

Consume the two rungs through the recipes exported from @vegastack/design rather than writing the classes by hand:

import { cn, surfaceInteractive, fillInteractive } from "@vegastack/design";

// A transparent control on a known surface: hover = rung 2, pressed = rung 3.
<button className={cn("rounded-md px-2", surfaceInteractive)} />;

// A control on an unknown backdrop, or one that hovers in its own hue.
<button
  className={cn(
    "bg-destructive-subtle text-destructive-text",
    fillInteractive.destructive,
  )}
/>;

--secondary, --muted, --accent and the whole --sidebar-* family are aliases of ladder rungs, kept so shadcn-shaped code keeps compiling. They have no independent values and must never be retuned on their own: secondary = muted = surface-1, accent = sidebar-accent = surface-2, sidebar = card.

One hairline, and it is an alpha

--border is derived, not authored: it is --foreground at --alpha-border (8% light / 14% dark). One translucent hairline therefore reads correctly on the page, on a card, inside a well and on a dark band, and it can never drift from the ink it tints. --input and --sidebar-border alias it.

Surfaces & text

--background
--foreground
--card
--card-foreground
--popover
--popover-foreground
--surface-1
--surface-2
--surface-3
--muted-foreground
--muted-foreground-faint
--border

Action — neutral ink

--primary
--primary-foreground
--primary-hover
--primary-active

Info — links & informational (the one chromatic accent)

--info
--info-foreground
--info-hover
--info-active
--info-subtle
--info-text

Destructive

--destructive
--destructive-foreground
--destructive-hover
--destructive-active
--destructive-subtle
--destructive-text

Success

--success
--success-foreground
--success-hover
--success-active
--success-subtle
--success-text

Warning

--warning
--warning-foreground
--warning-hover
--warning-active
--warning-subtle
--warning-text

Lines & utility

--border
--input
--ring
--track
--overlay

Charts — categorical series

--chart-1
--chart-2
--chart-3
--chart-4
--chart-5
--chart-6
--chart-7
--chart-8

Tags — categorical metadata (not status)

--tag-blue
--tag-blue-subtle
--tag-blue-text
--tag-cyan
--tag-cyan-subtle
--tag-cyan-text
--tag-green
--tag-green-subtle
--tag-green-text
--tag-lime
--tag-lime-subtle
--tag-lime-text
--tag-yellow
--tag-yellow-subtle
--tag-yellow-text
--tag-orange
--tag-orange-subtle
--tag-orange-text
--tag-red
--tag-red-subtle
--tag-red-text
--tag-pink
--tag-pink-subtle
--tag-pink-text
--tag-magenta
--tag-magenta-subtle
--tag-magenta-text
--tag-purple
--tag-purple-subtle
--tag-purple-text

Sidebar surface

--sidebar
--sidebar-foreground
--sidebar-primary
--sidebar-primary-foreground
--sidebar-accent
--sidebar-accent-foreground
--sidebar-border
--sidebar-ring

Per-family variants

Each chromatic family (info, destructive, success, warning) ships eight real tokens — no opacity-derived colors:

  • fill (--info) + foreground (--info-foreground) — the solid surface and its on-fill text.
  • hover / active (--info-hover, --info-active) — the interaction-state fills of the solid (hover:bg-info-hover active:bg-info-active).
  • subtle (--info-subtle) + text (--info-text) — the soft tinted background (badges, alerts) and the page-readable on-surface color.
  • subtle-hover / subtle-active (--info-subtle-hover, --info-subtle-active) — the hover and pressed steps of the soft fill. Both are precomposed at build time (the fill composited over subtle) and AA-gated against --info-text, because an alpha wash such as active:bg-info/(--alpha-pressed) would replace the tint rather than step it.

subtle, subtle-hover, subtle-active and text are theme-aware (distinct light/dark values); fill / hover / active / foreground are theme-independent — the same saturated mark with white text in both themes, so a bg-destructive button reads the same way on a light or dark page. The neutral action color mirrors this with --primary-hover / --primary-active, and --muted-foreground-faint is the deliberately sub-AA tone reserved for placeholders.

Charts — categorical series

The Charts group ships eight categorical tokens (--chart-1--chart-8), consumed as bg-chart-1, text-chart-2, fill-chart-3, etc. via the @theme inline bridge (--color-chart-N: var(--chart-N)). They exist to color categorical data series — one hue per series so adjacent slices, bars, or lines stay distinguishable — not as general-purpose accents.

Unlike the chromatic families, charts are theme-aware. The palette is intentionally categorical, not an artificial equiluminant ring: each series is tuned for separation in its theme, so lightness and chroma legitimately vary. Some direct OKLCH values exceed sRGB and are clipped only for the WCAG calculation; the build emits that gamut report instead of publishing duplicate *-p3 tokens. There is no per-token foreground; use surface tokens for text and axes and never rely on hue alone.

TokenLightDark
--chart-1oklch(0.546 0.245 262.88)oklch(0.623 0.214 259.81)
--chart-2oklch(0.6 0.118 184.7)oklch(0.696 0.17 162.48)
--chart-3oklch(0.398 0.07 227.39)oklch(0.769 0.188 70.08)
--chart-4oklch(0.646 0.222 41.12)oklch(0.75 0.183 55.93)
--chart-5oklch(0.645 0.246 16.44)oklch(0.645 0.246 16.44)
--chart-6oklch(0.505 0.213 27.52)oklch(0.69 0.21 25)
--chart-7oklch(0.6 0.127 104.2)oklch(0.72 0.15 103.9)
--chart-8oklch(0.531 0.182 256)oklch(0.72 0.13 256)

Assign chart colors by index in series order (chart-1, chart-2, …), not by meaning. For status semantics (success / warning / destructive) use the chromatic families instead, so the same hue carries the same meaning everywhere.

Tags — the label palette

The Tag group ships ten chromatic hues (blue, cyan, green, lime, yellow, orange, red, pink, magenta, purple), each a trio: --tag-{hue} (the accent — dots, icons), --tag-{hue}-subtle (chip fill) and --tag-{hue}-text (chip text). They mirror the chromatic-family shape but serve a different job: decorative labels (record categories, markets, custom tags), never status. The neutral tag uses muted / muted-foreground.

Both themes are AA-validated fail-closed in the token contrast gate: every -text clears 4.5:1 on its own -subtle fill and on the page surfaces, and every base clears 3:1 (WCAG 1.4.11) against the background. Consume them through TagGroup, which applies the bordered-tint chip formula (subtle fill + matching-hue hairline + hue text).

Hue is a label, not a signal. Status stays with the five semantic families (success / warning / destructive / info / neutral) so the same hue means the same thing everywhere; tags are free to color by category.

The --sidebar-* names are kept so shadcn-shaped code keeps compiling, but they are aliases of the ladder — the rail is not a second palette (it used to be, and --sidebar-accent quietly held the only real hover step in the system):

  • --sidebar = --card · --sidebar-foreground = --foreground — the rail is a surface like any other.
  • --sidebar-primary = --primary · --sidebar-primary-foreground = --primary-foreground — the active-item marker rail.
  • --sidebar-accent = --surface-2 — the hover rung. The current row rests one rung higher on --surface-3, so hovering an already-active row still moves.
  • --sidebar-border = --border · --sidebar-ring = --ring.

They stay bridged through @theme inline, so bg-sidebar, text-sidebar-foreground and border-sidebar-border all still compile — they simply resolve to the ladder now. New code should prefer the ladder names.

Beyond color

Color is one of several token families. The same semantic-token, light/dark, single-override model extends to the rest of the system:

  • Radius — one base radius (--radius, 0.75rem) plus the --radius-sm / --radius-md / --radius-lg scale, bridged to Tailwind rounded-*.
  • Elevation — borders-only by default; --shadow-overlay is the single shadow role, and it is for floating overlays alone.

Override model

Redefine a single variable in your app's global CSS and every component repaints — light and dark:

:root {
  --primary: oklch(0.55 0.2 264); /* your brand */
}

Because components reference --primary through the @theme inline bridge (--color-primary: var(--primary)), the override flows to bg-primary, the ladder's alpha washes (hover:bg-primary/(--alpha-hover) / active:bg-primary/(--alpha-pressed) — the fillInteractive.primary recipe), and focus outlines everywhere.

Do
Use semantic tokens: bg-primary, text-muted-foreground, border-border.
Don't
Hardcode hex or raw palettes: bg-[#18181b], bg-neutral-900.

The brand accent

--brand is the phosphor-green marker — theme-split (CX-9): oklch(0.6 0.17 148) in light (3.5:1 on background/card, so meaningful glyphs clear WCAG 1.4.11) and the full oklch(0.86 0.21 148) phosphor in dark (13.3:1). It is rationed to marker roles only: the live/AI-state dot, a sparkline endpoint, an eyebrow highlight, the terminal prompt glyph. Never fills, borders-at-rest, headlines, or buttons.

Brand and success share the green region deliberately (the brand IS a green); they are never distinguished by hue alone — an AI-live marker always pairs shape/motion/label with the color (pulse dot + "LIVE"/"REASONING" mono label), and success states always carry an icon. Under deuteranopia/protanopia simulation the pair keeps a luminance separation (ΔL ≈ 0.10 light / 0.28 dark) on top of those structural cues, and chart-7 was reassigned from green to olive (hue 104) so data series never collide with the marker.

On this page