Customization
Theming Overview
Understand VaneUI's powerful theming system and design token architecture.
VaneUI uses a powerful 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; // Simple 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
Function for programmatic theme modifications:
<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);}Baseline Inheritance
primary + outline matches the :root palette, so VaneUI omits data-appearance and data-variant for components resolving to those values. The component then inherits color variables from its nearest ancestor — this is what lets a default <Button> inside a filled <Card> automatically pick up the Card's text and background colors. Identity components (Mark, Chip, Link, Checkbox) deviate from baseline and always emit their own attributes. See Variant Inheritance for details.