Themes

Customize the look and feel of your application with built-in themes, colors, and dark mode support.

Available Themes

Volt UI comes with several pre-built color themes. Each theme provides a unique palette for primary, secondary, accent, and semantic colors.

V

Volt

Default blue theme

E

Ember

Warm orange theme

S

Sage

Natural green theme

D

Dusk

Purple twilight theme

G

Glacier

Cool cyan theme

Theme Setup

Add the theme provider to your application configuration:

import { provideVoltTheme } from '@voltui/components'; export const appConfig: ApplicationConfig = { providers: [ provideVoltTheme({ color: 'volt', style: 'soft', dark: false }) ] };

Color Options

Theme ColorsChoose your primary color palette
  • 'volt' - Blue (default)
  • 'ember' - Orange/Red
  • 'sage' - Green
  • 'dusk' - Purple
  • 'glacier' - Cyan
Style VariantsChoose your component style
  • 'sharp' - Small radius, flat shadows (default)
  • 'soft' - Large radius, diffuse shadows
  • 'brutal' - No radius, hard offset shadows, heavy borders
  • 'ghost' - No borders, no shadows
  • 'retro' - Y2K look

Runtime API

Two functions from @voltui/components control the theme at runtime. Both just set data-color, data-style and a .dark class on <html> — there is no hidden state, so you can inspect or override the result with plain DOM APIs at any time.

provideVoltTheme(options)Environment provider. Call it once in appConfig.providers to set the theme at bootstrap, on both server and client.

SSR-safe. It reads Angular's DOCUMENT injection token rather than the global document, so the theme attributes are present in the server-rendered HTML itself — no flash of the default theme while the client bundle hydrates.

applyVoltTheme(options, doc?)Plain function. Call it from a click handler, an effect, or anywhere else you need to change the theme after bootstrap (e.g. a theme switcher).

Browser-only by default (it no-ops if document is undefined). Pass an explicit doc argument to target a different document, e.g. in a test.

// app.config.ts — set the theme at bootstrap
import { provideVoltTheme } from '@voltui/components';

export const appConfig: ApplicationConfig = {
  providers: [provideVoltTheme({ color: 'ember', style: 'soft' })],
};

Dark Mode Strategy

Dark mode is class-based: adding .dark to <html> switches every component to its dark token values (see core.css's @custom-variant dark). Volt does not read prefers-color-scheme automatically — dark defaults to light unless you pass dark: true or call applyVoltTheme yourself, so the choice is always explicit and won't silently change if the visitor's OS theme changes.

// Static: dark by default
provideVoltTheme({ color: 'volt', dark: true })

// Dynamic: toggle from a button, a signal, etc.
import { applyVoltTheme } from '@voltui/components';
applyVoltTheme({ dark: true });

// Opt in to following the OS preference yourself
const media = matchMedia('(prefers-color-scheme: dark)');
applyVoltTheme({ dark: media.matches });
media.addEventListener('change', e => applyVoltTheme({ dark: e.matches }));

The docs app itself persists the visitor's choice to localStorage and applies it from a small inline script in index.html's <head>, before any stylesheet loads — see src/app/components/theme-switcher.ts for a reference implementation if you want the same no-flash behavior client-side.

CSS Variables

Every preset sets the same set of semantic tokens as oklch() values. Components consume them through Tailwind utilities (bg-primary, text-muted-foreground) mapped via @theme inline in core.css — never a raw var(--primary). This is the full list a preset can define; see the two guides below for copy-paste templates.

/* Color tokens — one block per data-color, one per .dark[data-color] */
--background / --foreground
--surface / --surface-foreground        /* cards, popovers, dropdowns */
--muted / --muted-foreground
--border / --input / --ring             /* --input is the form-control border */
--primary / --primary-foreground
--secondary / --secondary-foreground
--destructive / --destructive-foreground /* shared by all colors, only in core.css */
--success / --success-foreground
--warning / --warning-foreground
--error / --error-foreground
--info / --info-foreground

/* Style tokens — one block per data-style */
--radius
--border-width / --border-style
--ring-width / --ring-offset-width
--volt-shadow-sm / --volt-shadow / --volt-shadow-md / --volt-shadow-lg
--spacing-component / --spacing-gap
--volt-font-weight-base / --volt-font-weight-heading / --volt-font-weight-label

Create a Custom Color Preset

A color preset is just a pair of selectors keyed by data-color. Copy this template, replace 'mytheme' and the values, then import the file after @voltui/components/themes.css in your global stylesheet.

:root[data-color='mytheme'] {
  --primary: oklch(0.58 0.2 265);
  --primary-foreground: oklch(0.98 0 0);
  --secondary: oklch(0.96 0.01 265);
  --secondary-foreground: oklch(0.15 0.01 265);
  --background: oklch(1 0 0);
  --foreground: oklch(0.15 0.01 265);
  --surface: oklch(1 0 0);
  --surface-foreground: oklch(0.15 0.01 265);
  --muted: oklch(0.96 0.005 265);
  --muted-foreground: oklch(0.5 0.02 265);
  --border: oklch(0.9 0.01 265);
  --input: oklch(0.65 0.02 265);
  --ring: oklch(0.58 0.2 265);
  --success: oklch(0.5 0.14 150);
  --success-foreground: oklch(0.98 0 0);
  --warning: oklch(0.7 0.16 70);
  --warning-foreground: oklch(0.2 0.05 70);
  --error: oklch(0.55 0.22 25);
  --error-foreground: oklch(0.98 0 0);
  --info: oklch(0.55 0.14 240);
  --info-foreground: oklch(0.98 0 0);
}

.dark[data-color='mytheme'] {
  /* same tokens, tuned for a dark --background/--surface */
}

Keep every foreground/background pair at 4.5:1 contrast or better (3:1 for --ring and --input against --background) — see scripts/contrast-audit.mjs in the repo for the exact check this project runs against its own presets. The Create Theme tool generates a starting point from a single accent color if you'd rather not hand-pick eleven tokens.

Create a Custom Style Preset

A style preset never touches color — only shape. It's keyed by data-style and applies to every color at once.

:root[data-style='mystyle'] {
  --radius: 0.5rem;
  --border-width: 1px;
  --border-style: solid;
  --ring-width: 2px;
  --ring-offset-width: 2px;

  --volt-shadow-sm: 0 1px 2px 0 oklch(0 0 0 / 0.05);
  --volt-shadow: 0 1px 3px 0 oklch(0 0 0 / 0.1);
  --volt-shadow-md: 0 4px 6px -1px oklch(0 0 0 / 0.1);
  --volt-shadow-lg: 0 10px 15px -3px oklch(0 0 0 / 0.1);

  --spacing-component: 1;
  --spacing-gap: 0.5rem;

  --volt-font-weight-base: 400;
  --volt-font-weight-heading: 600;
  --volt-font-weight-label: 500;
}

Registration is the same as a color preset: put it in your global stylesheet after @voltui/components/themes.css, then set data-style="mystyle" via provideVoltTheme or applyVoltTheme.