VaneUI

VaneUI

Customization

Theming Overview

Understand VaneUI's powerful theming system and design token architecture.

Edit this page

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:

  • tag: Default HTML element (e.g., "button", "div")
  • base: Base CSS classes always applied
  • themes: Tree of BaseTheme subclasses that generate CSS classes
  • defaults: Default prop values
  • categories: Which prop categories the component uses
react-icon
// Simplified view of how a component theme is structured
const 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:

react-icon
// FontSizeTheme returns consumer class for font size
class FontSizeTheme extends BaseTheme {
getClasses(extractedKeys) {
return ["text-(length:--fs)"]; // Consumes --fs CSS variable
}
}
// SimpleConsumerTheme returns classes that consume color variables
class 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:

react-icon
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:

  • iconButton, badge, icon, chip, code, kbd, mark, input, label, img
  • button — compound: button.main, button.spinner
  • checkbox — compound: checkbox.input, checkbox.check, checkbox.indeterminate, checkbox.wrapper

Layout:

  • divider, container, row, col, stack, section
  • grid2, grid3, grid4, grid5, grid6
  • card — compound: card.main, card.header, card.body, card.footer

Typography:

  • text, title, pageTitle, sectionTitle, blockquote, link, list, listItem

Overlay / Floating:

  • overlay — Overlay backdrop theme
  • popup — Popup floating element theme
  • modal — compound: modal.content, modal.overlay, modal.header, modal.body, modal.footer, modal.closeButton
  • menu — compound: menu.item, menu.popup, menu.divider, menu.label
  • navLink — compound: navLink.root, navLink.label

ThemeProvider Props

themeDefaults

Set default prop values for components:

react-icon
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:

react-icon
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:

react-icon
<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:

react-icon
// 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:

css-icon
/* 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.

Flow Summary

  1. User writes: <Button danger lg filled>Click</Button>
  2. Button component calls useTheme() to get theme.button.main
  3. ThemedComponent calls theme.getComponentConfig(props)
  4. Props merged with defaults, then extracted by category: { size: 'lg', appearance: 'danger', variant: 'filled' }
  5. Theme tree is walked, each BaseTheme.getClasses() returns CSS classes
  6. Classes are merged with twMerge(), data attributes are added (because danger + filled deviate from the primary + outline baseline)
  7. Final render: <button class="..." data-vane-type="ui" data-size="lg" data-appearance="danger" data-variant="filled">
  8. CSS rules in rules.css set unit variables and the appearance/variant palette
  9. Browser computes final styles from CSS variables