Search...

Search documentation...

Theming

Theming

Vesuvius UI separates two independent axes: light/dark mode and color theme (palette). Both are driven by ThemeProvider, mounted once at your app's root.

import { ThemeProvider } from "@pompeitech/vesuvius-ui";
 
<ThemeProvider defaultTheme="system" defaultColorTheme="lava">
  <App />
</ThemeProvider>;

API

ThemeProvider

PropTypeDefaultDescription
defaultTheme"light" | "dark" | "system""system"Mode applied before the user picks one. "system" follows prefers-color-scheme and keeps listening for OS-level changes.
defaultColorThemeColorTheme"lava"Color theme applied before the user picks one.
colorThemesreadonly ColorTheme[]all 10 built-insThe full set of selectable color themes — pass a subset to restrict what ThemePalettePicker/ThemeSwitcher offer, or append your own custom theme name (see A custom theme).
storageKeystring"vesuvius-ui-theme"localStorage key for the mode choice.
colorThemeStorageKeystring"vesuvius-ui-color-theme"localStorage key for the color theme choice.

useTheme()

FieldTypeDescription
theme"light" | "dark" | "system"What the user picked.
resolvedTheme"light" | "dark"theme with "system" resolved to an actual value — use this for anything that needs a concrete mode.
colorThemeColorThemeThe active palette name.
setTheme(theme: "light" | "dark" | "system") => void
setColorTheme(theme: ColorTheme) => void

Throws if called outside a ThemeProvider — it doesn't fail silently with undefined values.

Mode is applied as a .dark class on <html>; the color theme as a data-theme="<name>" attribute, also on <html>. Both persist to localStorage and stay fully independent — toggling dark mode never changes the color theme, and vice versa, so a user's "supabase, dark" or "vercel, light" combination survives a reload.

Theme tokens

Every color a component uses is a CSS custom property, never a hardcoded Tailwind color — so re-theming the kit is entirely a CSS-variable exercise, no component code involved. This is the exact convention shadcn/ui uses, and Vesuvius follows it deliberately: each raw token (--primary, --background, ...) is set once under :root/.dark, then re-exposed to Tailwind v4 through a @theme inline block:

@theme inline {
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  /* ...one line per token, same pattern for all of them */
}

That --color-* indirection is what makes bg-primary, text-primary-foreground, border-border, ring-ring, and every other themed utility class exist at all — components author with those utilities and never see the raw --primary value directly, so overriding a token instantly re-themes every component that uses it, with zero component-level changes.

Base tokens

TokenUtility classUsed for
--backgroundbg-backgroundPage background, behind every surface.
--foregroundtext-foregroundDefault body text color.
--cardbg-cardCard/panel surface background.
--card-foregroundtext-card-foregroundText on a card surface.
--popoverbg-popoverDropdown/popover/tooltip surface background.
--popover-foregroundtext-popover-foregroundText on a popover surface.
--primarybg-primaryPrimary action color — default Button, active states, brand accent.
--primary-foregroundtext-primary-foregroundText/icon color on top of --primary.
--primary-emphasistext-primary-emphasisAccessible primary text/icons on light surfaces.
--secondarybg-secondarySecondary surfaces — the secondary Button variant, subtle fills.
--secondary-foregroundtext-secondary-foregroundText on --secondary.
--mutedbg-mutedLow-emphasis backgrounds — disabled states, subtle rows.
--muted-foregroundtext-muted-foregroundLow-emphasis text — captions, placeholders, helper text.
--accentbg-accentHover/highlight background — menu items, selected rows.
--accent-foregroundtext-accent-foregroundText on --accent.
--destructivebg-destructiveDestructive actions and error states.
--destructive-foregroundtext-destructive-foregroundText/icon color on top of --destructive.
--borderborder-borderDefault border color, used everywhere (cards, inputs, dividers).
--inputborder-inputBorder color specifically for form controls (Input, Select, Textarea, ...).
--ringring-ringFocus ring color on every focusable element.

Semantic status tokens

Beyond shadcn's own convention, Vesuvius adds four dedicated semantic-status pairs — used by Alert, Badge, Toast and anywhere else a status needs a fixed meaning regardless of the active color theme's --primary:

TokenUtility classUsed for
--success / --success-foregroundbg-success / text-success-foregroundConfirmations, completed states.
--success-emphasistext-success-emphasisAccessible success text/icons on light surfaces.
--warning / --warning-foregroundbg-warning / text-warning-foregroundCaution, approaching-limit states.
--warning-emphasistext-warning-emphasisAccessible warning text/icons on light surfaces.
--info / --info-foregroundbg-info / text-info-foregroundNeutral informational callouts.
--info-emphasistext-info-emphasisAccessible informational text/icons on light surfaces.
--highlight / --highlight-foregroundbg-highlight / text-highlight-foreground"New"/announcement-style emphasis, distinct from --primary.
--highlight-emphasistext-highlight-emphasisAccessible highlight text/icons on light surfaces.

Chart tokens

TokenUtility classUsed for
--chart-1 … --chart-5fill-chart-1 … fill-chart-5, etc.A 5-step categorical color scale for Charts, SimpleRadarChart, Sparkline, ChartCard — cycled through in order as series are added.

The Sidebar component gets its own dedicated palette instead of reusing the base tokens, so a sidebar can intentionally read as a distinct "chrome" region (darker, or differently-tinted) from the main content area:

TokenUtility classUsed for
--sidebarbg-sidebarSidebar background.
--sidebar-foregroundtext-sidebar-foregroundDefault sidebar text.
--sidebar-primary / --sidebar-primary-foregroundbg-sidebar-primary / text-sidebar-primary-foregroundActive/selected sidebar item.
--sidebar-accent / --sidebar-accent-foregroundbg-sidebar-accent / text-sidebar-accent-foregroundHovered sidebar item.
--sidebar-borderborder-sidebar-borderBorders within the sidebar (including its own outer edge).
--sidebar-ringring-sidebar-ringFocus ring on sidebar items.

Radius and typography

TokenDescription
--radiusThe base corner radius. Every component radius (--radius-sm, --radius-md, --radius-lg, --radius-xl) is derived from this one value via calc(), so changing just --radius rescales every rounded corner in the kit consistently.
--font-headingFont family applied to <h1>–<h6> and any component that opts into the "heading" text style. Body text uses Tailwind's default font-sans stack, unaffected by this token.

Override any of these at :root (and .dark for the dark-mode value) in your own app's CSS, after Vesuvius's stylesheet, to rebrand without forking the package:

:root {
  --radius: 0.5rem;
  --font-heading: "Your Brand Font", sans-serif;
}

The 10 built-in themes

The visual catalog lives at /themes, with a dedicated detail page for every palette. Every theme is also its own exports subpath under themes/ — import only the ones you offer, to ship a smaller CSS bundle:

/* All themes at once */
@import "@pompeitech/vesuvius-ui/styles.css";
 
/* Or only what you need */
@import "@pompeitech/vesuvius-ui/base.css";
@import "@pompeitech/vesuvius-ui/themes/lava.css";

lava

The kit's flagship theme and ThemeProvider's default — re-tints every surface (not just the accent), with a warm volcanic-stone palette: dark ink on cream surfaces in light mode, cream ink on near-black obsidian in dark mode, both anchored to a burnt-orange primary.

@import "@pompeitech/vesuvius-ui/themes/lava.css";

stripe

A fintech-editorial palette inspired by Stripe: a white canvas, deep navy ink and a focused violet-blurple action color.

@import "@pompeitech/vesuvius-ui/themes/stripe.css";

vercel

A sharp black-and-white palette inspired by Vercel's minimal product surfaces.

@import "@pompeitech/vesuvius-ui/themes/vercel.css";

supabase

An emerald-mint palette inspired by Supabase: bright, technical and friendly.

@import "@pompeitech/vesuvius-ui/themes/supabase.css";

linear

A restrained cool palette with the soft violet-indigo primary associated with Linear.

@import "@pompeitech/vesuvius-ui/themes/linear.css";

claude

A warm parchment and terracotta palette inspired by Claude's reading-room feel.

@import "@pompeitech/vesuvius-ui/themes/claude.css";

amber-minimal

A clean white palette with a focused amber-gold primary accent.

@import "@pompeitech/vesuvius-ui/themes/amber-minimal.css";

claymorphism

A soft dimensional palette built around a mid-tone clay canvas (#e0e0e0), near-white puffed surfaces and a bold violet primary. The contrast and semantic tokens stay compatible with the standard shadcn contract in both modes.

@import "@pompeitech/vesuvius-ui/themes/claymorphism.css";

alpine

A premium SaaS palette inspired by Alpine: deep alpine-night ink, confident cobalt structure, coral warmth and a soft blush canvas (#fcf5f7).

@import "@pompeitech/vesuvius-ui/themes/alpine.css";

aubergine

A Slack-inspired palette with an aubergine sidebar, expressive violet surfaces and bright blue actions.

@import "@pompeitech/vesuvius-ui/themes/aubergine.css";

What a theme actually overrides

A theme file is CSS custom properties scoped under [data-theme="<name>"] (and [data-theme="<name>"].dark for its dark variant). All ten built-in themes provide a complete light/dark palette: surfaces, text, actions, semantic statuses, chart colors and sidebar tokens.

Every component reads these as Tailwind utilities (bg-primary, text-muted-foreground, border-border, …) generated from the @theme inline block in base.css — components never hardcode a color, so a theme swap needs no component-level changes at all.

Letting users switch themes

Three ready-made controls read/write the same ThemeProvider context:

import { ThemeModeToggle, ThemePalettePicker, ThemeSwitcher } from "@pompeitech/vesuvius-ui";
 
// A single icon button that flips light/dark.
<ThemeModeToggle />
 
// A searchable command palette of color themes.
<ThemePalettePicker />
 
// Both mode and color theme in one dropdown.
<ThemeSwitcher />

All three call useTheme() internally, so they only work inside a ThemeProvider.

A custom theme

A theme is just CSS custom properties under [data-theme="<name>"] (and [data-theme="<name>"].dark for its dark variant) — add your own without touching this package. Override the neutral surface tokens as well as the accent tokens if you want the same complete light/dark treatment as the built-ins.

[data-theme="acme"] {
  --primary: oklch(0.55 0.2 145);
  --primary-foreground: oklch(0.985 0 0);
  --ring: oklch(0.55 0.2 145);
 
  --chart-1: oklch(0.55 0.2 145);
  --chart-2: oklch(0.62 0.18 170);
  --chart-3: oklch(0.5 0.19 120);
  --chart-4: oklch(0.68 0.16 90);
  --chart-5: oklch(0.45 0.18 160);
 
  --sidebar-primary: oklch(0.55 0.2 145);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-ring: oklch(0.55 0.2 145);
}
[data-theme="acme"].dark {
  --primary: oklch(0.7 0.17 145);
  --primary-foreground: oklch(0.145 0 0);
  --ring: oklch(0.7 0.17 145);
 
  --chart-1: oklch(0.7 0.17 145);
  --chart-2: oklch(0.75 0.15 170);
  --chart-3: oklch(0.65 0.16 120);
  --chart-4: oklch(0.8 0.14 90);
  --chart-5: oklch(0.6 0.15 160);
 
  --sidebar-primary: oklch(0.7 0.17 145);
  --sidebar-primary-foreground: oklch(0.145 0 0);
  --sidebar-ring: oklch(0.7 0.17 145);
}

Then pass its name through colorThemes so the built-in pickers list it too:

import { ThemeProvider, COLOR_THEMES } from "@pompeitech/vesuvius-ui";
 
<ThemeProvider colorThemes={[...COLOR_THEMES, "acme"]} defaultColorTheme="acme">
  <App />
</ThemeProvider>;

colorTheme's type (ColorTheme) is the 10 built-in names widened with string & {} specifically so a custom name like "acme" type-checks without needing to fork or extend the package's own types.

Prefer oklch() over hsl()/rgb() when writing new token values — every built-in theme is authored in oklch() specifically because it keeps perceived lightness consistent across hues, which is what makes a light/dark pair for the same theme feel like a matched set instead of two unrelated colors.

Defending components rendered inside another host's CSS

If this kit's components render inside a page that has its own global CSS (a docs site, a CMS theme, another design system on the same page) — not just inside your own app — that host's CSS can leak into anything the kit renders as a raw native element, especially <table> (Calendar, Table/DataTable), since a surprising number of CSS resets ship an unscoped global table { ... } rule.

The kit defends against this itself, in base.css's @layer base, rather than leaving it to consumers: [data-slot="table"] and .group\/calendar table get !important-guarded resets for display, border-collapse, and cell borders/padding/background. !important is required specifically because of how CSS cascade layers work — Tailwind's own utility classes live in @layer utilities, and any unlayered host CSS (even a single low-specificity selector) always outranks anything in a layer, regardless of specificity; only !important reverses that. If you hit the same class of bug with a component this kit doesn't yet guard, the fix is the same shape: an unlayered (or !important) rule scoped to that component's data-slot.