VaneUI uses a theme system based on ComponentTheme classes that define styling for each component. This page explains the theme architecture and how to customize it.
Theme architecture
ComponentTheme class
Each component has a ComponentTheme instance that defines:
// Simplified view of how a component theme is structuredconst buttonTheme = new ComponentTheme( "button", // Default tag "vane-button", // Base classes (kept minimal) { size: { px, py, text, gap }, // Size-related themes appearance: { bg, text, border }, // Appearance themes layout: { radius, border, ring } // Layout themes }, buttonDefaults, // Imported from buttonDefaults.ts BUTTON_CATEGORIES // Prop categories);BaseTheme subclasses
Each BaseTheme subclass generates specific CSS classes based on extracted props:
// FontSizeTheme returns consumer class for font sizeclass FontSizeTheme extends BaseTheme { getClasses(extractedKeys) { return ["text-(length:--fs)"]; // Consumes --fs CSS variable }}
// SimpleConsumerTheme returns classes that consume color variablesclass SimpleConsumerTheme extends BaseTheme { getClasses(extractedKeys) { if (!extractedKeys.appearance) return []; return ["bg-(--bg-color)", "text-(--text-color)"]; }}Accessing the theme
Use the useTheme hook to access the current theme:
import { useTheme } from '@vaneui/ui';
function CustomComponent() { const theme = useTheme();
// Compound themes are nested by sub-part const buttonMainTheme = theme.button.main; const cardMainTheme = theme.card.main; // Single-target themes are accessed directly const badgeTheme = theme.badge;
return <div>Custom component</div>;}Available component themes
VaneUI includes themes for all components.
Interactive:
Layout:
Typography:
Overlay / Floating:
ThemeProvider props
themeDefaults
Set default prop values for components:
import { ThemeProvider, type ThemeDefaults } from '@vaneui/ui';
const defaults: ThemeDefaults = { button: { main: { filled: true, // change variant from outline (built-in) to filled lg: true, // larger than built-in sm }, }, card: { main: { shadow: true }, // add shadow (not a default) },};
<ThemeProvider themeDefaults={defaults}> <Button>Large filled button</Button> <Card>Card with shadow</Card></ThemeProvider>extraClasses
Add additional CSS classes based on active props:
import type { ThemeExtraClasses } from '@vaneui/ui';
const extraClasses: ThemeExtraClasses = { button: { main: { primary: 'shadow-lg hover:shadow-xl transition-shadow', danger: 'animate-pulse', }, }, card: { main: { filled: 'backdrop-blur-sm' }, },};
<ThemeProvider extraClasses={extraClasses}> <Button primary>Button with shadow</Button> <Button danger>Pulsing danger button</Button></ThemeProvider>themeOverride
A function with direct access to the full theme, for changes the declarative options can't express (like appending to a component's base classes):
<ThemeProvider themeOverride={(theme) => { // Modify button base classes theme.button.main.base += ' uppercase tracking-wide';
// Modify defaults theme.button.main.defaults = { ...theme.button.main.defaults, bold: true, };
return theme;}}> <App /></ThemeProvider>mergeStrategy
Control how nested ThemeProviders combine:
// Default: 'merge' - child theme merges with parent<ThemeProvider themeDefaults={{ button: { main: { lg: true } } }}> <ThemeProvider themeDefaults={{ button: { main: { filled: true } } }}> {/* Button gets both lg AND filled */} <Button>Large Filled</Button> </ThemeProvider></ThemeProvider>
// 'replace' - child theme replaces parent entirely (resets to defaultTheme + child)<ThemeProvider themeDefaults={{ button: { main: { lg: true } } }}> <ThemeProvider themeDefaults={{ button: { main: { sm: true } } }} mergeStrategy="replace" > {/* Button is small only (parent's lg is ignored) */} <Button>Small Only</Button> </ThemeProvider></ThemeProvider>Data attributes
Components emit data attributes that CSS rules use for styling:
<button class="vane-button text-(length:--fs) py-(--py) ..." data-vane-type="ui" data-size="md" data-appearance="danger" data-variant="filled"> Click me</button>CSS rules in rules.css set unit variables per data-size and per-component class. --fs-unit, --py-unit, and (for Icon) --icon-size are all set together so font-size, padding, gap, and border-radius scale together:
/* Per-component size mapping */.vane-button[data-size="md"] { --fs-unit: var(--fs-unit-md); --py-unit: 2;}
/* Icon uses a decoupled --icon-size, not --fs */.vane-icon[data-size="md"] { --fs-unit: var(--fs-unit-md); --icon-size: calc(var(--spacing) * 8); --py-unit: 2;}
/* Appearance + variant set the color palette */[data-variant="filled"][data-appearance="danger"] { --text-color: var(--color-text-filled-danger); --bg-color: var(--color-bg-filled-danger);}Color inheritance
Components with a concrete appearance emit data-appearance and data-variant and paint their own colors, so a default <Button> inside a filled <Card> keeps its own primary-outline palette. Inheritance is opt-in: only inherit-mode components (the typography components, which default to appearance="inherit") and appearance-less Icon emit nothing and read their colors from the nearest ancestor. The :root palette is the fallback those inherit-mode components use when no ancestor sets colors. See Variant Inheritance for details.