Themes & Tokens
Every Vueform UI stylesheet reads its colors, spacing, borders, and typography from --vf-* CSS variables and defines none of them itself. A Theme is the JavaScript map of Tokens that fills those variables in, injected into the page when Vueform installs. The stylesheets are identical across Themes and only the Token values differ, so restyling means changing values, never fighting selectors — to change which classes components render instead, see Classes.
Tokens
Open any Vueform stylesheet and the contract is visible — every visual decision is a variable:
.vf-input {
color: var(--vf-input-color);
background: var(--vf-input-bg);
border-radius: var(--vf-input-radius);
}Token keys are camelCase and map one-to-one to those variables — inputColor becomes --vf-input-color, with digits splitting into their own segment (gray300 → --vf-gray-300).
The resolved global Tokens are written as :root declarations into a single <style id="vueform-theme"> tag at the start of <head>. Because the tag comes first in <head>, any stylesheet of yours overrides it, so declaring a --vf-* variable in your own CSS is a valid override too, useful when the value already lives in a stylesheet.
The tag's id is configurable via build.styleId, a CSP nonce is attached as described in Style Tag Nonce, and server rendering extracts the same CSS via Style Injection.
Choosing a Theme
The entry you import createVueform from picks the Theme. The Full Bundle (@vueform/ui/bundle) — the entry the rest of the docs import from — ships the default Theme, and importing from a Theme-named entry bundles that Theme instead:
import { createVueform } from '@vueform/ui/material'
app.use(createVueform())A Modular Setup bundles no Theme at all — import the Token map from @vueform/ui/themes and pass it as the theme option.
There are no CSS imports to manage in either setup — each component entry carries its own stylesheet chunks, while <link> and CDN setups use the flattened @vueform/ui/theme.css. For what the Theme CSS weighs — and how to import components without it — see Code-Splitting.
Available Themes
| Name | Identifier | Description |
|---|---|---|
| Default | primary | Vueform's default look. |
| Material | material | In progress — currently shares the default Token set as a placeholder. |
The identifier names the Theme's export from @vueform/ui/themes. The default Theme ships in the Full Bundle itself (@vueform/ui/bundle), and every other Theme gets a Full Bundle entry of its own (@vueform/ui/{identifier}).
Overriding Tokens
Most projects don't need a new Theme, just a handful of different Token values. The tokens option overrides individual Tokens of whichever Theme is active:
createVueform({
tokens: {
brand: '#0ea5e9',
gutter: '1.5rem',
inputinputBg(property) inputBg?: string | number | undefinedinputBorderColorinputColorinputRadius...
},
})
Top-level keys replace the Theme's value, omitted keys keep it, and a dark block merges separately with the Theme's own dark block. The option works the same on the Full Bundle and next to a theme option in a Modular Setup.
Token Tiers
The built-in Token set is organized in three tiers — primitive, semantic, and component — each built from references to the tier below, so a change low down ripples up through everything that references it. To see exactly how far a Token reaches, hover its name anywhere in the docs — the popup lists the CSS classes that read its variable directly and the Tokens that reference it.
Primitives
Primitives are raw values with no references — the color scale, spacing, radii, typography. Overriding one moves every Token built on it. Take gutter — elementsGutterY, elementsGutterX, groupGutterY, and listGutterY all default to $gutter, so the single override above moves every gap between elements, groups, and list rows, while any of the specific Tokens can still be set on top.
| CSS Variable | Description |
|---|---|
--vf-brand | Brand primary color. |
--vf-red | Red accent color used for danger and error states. |
--vf-green | Green accent color used for success states. |
--vf-sky | Sky blue accent color used for info states. |
--vf-orange | Orange accent color used for warning states. |
--vf-purple | Purple accent color used for help states. |
--vf-white | White. |
--vf-black | Black. |
--vf-gray-50 | Neutral gray, 5% dark. |
--vf-gray-100 | Neutral gray, 10% dark. |
--vf-gray-200 | Neutral gray, 20% dark. |
--vf-gray-300 | Neutral gray, 30% dark. |
--vf-gray-400 | Neutral gray, 40% dark. |
--vf-gray-500 | Neutral gray, 50% dark. |
--vf-gray-600 | Neutral gray, 60% dark. |
--vf-gray-700 | Neutral gray, 70% dark. |
--vf-gray-800 | Neutral gray, 80% dark. |
--vf-gray-900 | Neutral gray, 90% dark. |
| CSS Variable | Description |
|---|---|
--vf-gutter | Default gutter used between elements, groups, list rows, and grid cells. |
--vf-space-1 | Small spacing unit. |
--vf-space-2 | Medium spacing unit. |
--vf-space-3 | Large spacing unit. |
--vf-space-hr | Vertical spacing around horizontal rules. |
--vf-px | Default horizontal padding for inputs and buttons. |
--vf-py | Default vertical padding for inputs and buttons. |
| CSS Variable | Description |
|---|---|
--vf-border-width-t | Top border width. |
--vf-border-width-r | Right border width. |
--vf-border-width-b | Bottom border width. |
--vf-border-width-l | Left border width. |
--vf-radius | Default border radius. |
--vf-radius-sm | Smaller border radius for tight elements. |
--vf-radius-lg | Larger border radius for loose elements. |
| CSS Variable | Description |
|---|---|
--vf-icon-add | SVG used for "add" affordances (e.g. list add buttons). |
--vf-icon-remove | SVG used for "remove" affordances. |
--vf-icon-sort | SVG used for the list sort handle. |
--vf-icon-loading | SVG used for the loading spinner. |
Semantic Tokens
Semantic Tokens are purpose-named and reference primitives — bgDisabled feeds the disabled backgrounds of inputs and choices, textSoft feeds help and description text. Overriding one restyles that purpose everywhere it appears, written like any other key in the tokens map.
| CSS Variable | Description |
|---|---|
--vf-text-disabled | Text color of disabled controls. |
--vf-bg-disabled | Background of disabled inputs and choices. |
--vf-border-disabled | Border color of disabled inputs and choices. |
| CSS Variable | Description |
|---|---|
--vf-text-soft | Muted text color used by help and description. |
--vf-link-color | Color used for links inside form text. |
| CSS Variable | Description |
|---|---|
--vf-icon-width | Default width for inline icons. |
--vf-icon-height | Default height for inline icons. |
Component Tokens
Component Tokens are scoped to one component group — input, button, list, and so on — and reference primitives and semantic Tokens. They are the place to restyle a single component without touching the rest, and each component's reference page lists its own under a Tokens tab — see VfList for an example.
The tiers are a convention, not a mechanism. Primitives are concrete, semantic Tokens reference primitives, and component Tokens reference the two tiers below them — nothing enforces the layering, but following it in your own overrides and custom Tokens keeps every override's reach predictable.
Token References
A $name inside a Token value becomes var(--vf-name) in the generated CSS, so references compose anywhere CSS accepts a value — inside calc(), color-mix(), or nested expressions. The built-in set uses them throughout:
{
inputBorderColor: '$gray300',
fileGutterX: 'calc($gutter / 2)',
btnBgPrimaryHover: 'color-mix(in srgb, $brand 100%, black 15%)',
}:root {
--vf-input-border-color: var(--vf-gray-300);
--vf-file-gutter-x: calc(var(--vf-gutter) / 2);
--vf-btn-bg-primary-hover: color-mix(in srgb, var(--vf-brand) 100%, black 15%);
}References are written in camelCase — $borderWidthT, never $border-width-t — and work anywhere a Token value can be set, global or scoped, built-in or custom.
The one hard rule is that references must not form a cycle — cyclic variables are invalid CSS and the browser discards them.
Per-Form and Per-Element Tokens
Token overrides don't have to be global. The same map the tokens option takes works as a tokens prop on VfForm and on every element — buttons included — affecting the component and everything rendered inside it, with a nested element's own tokens winning over an inherited override:
<template>
<VfForm :tokens="{ inputBg: '#f8fafc' }">
<VfText name="name" label="Name" />
<VfText name="email" label="Email" :tokens="{ inputBg: '#ffffff' }" />
</VfForm>
</template>Under the hood the overrides become a per-instance vf-t-* class plus a small <style> tag rendered by the component — never inline style attributes — with the CSP Nonce applied when configured.
Only forms and elements take the prop. Sub-components such as VfNav, VfTabs, or VfSteps inherit their Tokens from the surrounding form.
Because the prop is schema-reachable, keys or values that could break out of the scoped rule — a key that doesn't normalize to a plain --vf-* name, a value containing { or } — are dropped with a dev warning. The policy is part of security's Always-On Guards.
Dark Mode
Each built-in Theme carries a dark block — overrides for the gray scale and the component colors that must flip, from inputs and choices to buttons and tabular cells — emitted as a separate CSS block that applies while dark mode is active.
The darkMode option decides when that is. The default 'system' wraps the block in a prefers-color-scheme: dark media query, while a subselector emits it as :root<subselector>, following however your app toggles dark mode:
createVueform({
darkMode: '.dark', // dark while <html> has the class
})The selector is matched against <html> itself — the block becomes :root.dark, not a descendant rule — which fits the class-on-html convention of Tailwind and most theme switchers, and '[data-theme="dark"]' works the same way. The option reference lives at darkMode.
Any tokens map — the config option, a Theme, a tokens prop — takes a dark block of its own:
createVueform({
tokens: {
brand: '#0f766e',
dark: {
brand: '#14b8a6',
},
},
})How the light and dark values relate differs by level. Globally, a light override never reaches into dark mode — the Theme's own dark block still wins there — so a Token the Theme also defines under dark (the gray scale, for instance) needs both overrides. On forms and elements it's the reverse — the light override stays in effect in dark mode too, and the dark block only layers on top.
Custom Tokens
Token keys are open — an unknown key becomes a --vf-* variable on :root just like the built-ins, which makes the Theme a natural home for app-specific values:
createVueform({
tokens: {
brandAccent: '#f59e0b',
inputBorderColor: '$brandAccent',
},
})brandAccent is now a real variable — built-in Tokens can reference it, and your own stylesheets can read it:
.badge { background: var(--vf-brand-accent); }Type Augmentation
ThemeTokens — the type behind the theme and tokens options and the tokens prop — has an open index signature, so custom keys are accepted as-is. Augmenting it adds autocompletion and value typing on top, and one augmentation covers all three places:
// vueform-tokens.d.ts
declare module '@vueform/core' {
interface ThemeTokens {
brandAccent?: string
}
}With the augmentation in place, the custom key turns up in every Token map's completions like any built-in:
createVueform({
tokens: {
brandbrandbrandAccent(property) brandAccent?: string | undefined...
},
})Custom Themes
When overrides grow into a reusable look shared across apps, package them as a Theme and pass it to the theme option.
Extending a Theme
extendTheme() layers overrides on top of an existing Theme and returns a new one without mutating the base — the same merge the tokens option performs, for when the result should live in its own module:
import { extendTheme, primary } from '@vueform/ui/themes'
export const corporate = extendTheme(primary, {
brand: '#0f766e',
radius: '0px',
dark: {
brand: '#14b8a6',
},
})Pass the result to the theme option like any built-in Theme.
Creating a Theme from Scratch
The defineTheme() helper exported from @vueform/ui/themes types a complete Theme. Vueform's built-in components' stylesheets read a fixed vocabulary of Tokens — everything listed in Token tiers — so a from-scratch Theme must supply them all, and defineTheme() makes each a required key (a mistyped one surfaces as the real key missing).
Values are string | number, an explicit dark block is required ({} opts out), and extra custom Tokens are accepted:
import { defineTheme } from '@vueform/ui/themes'
export const slate = defineTheme({
brand: '#334155',
gutter: '1rem',
// …the rest of the token set
dark: {},
})Custom Token Structures
The root defineTheme() imported directly from @vueform/ui accepts any Token vocabulary and only requires the dark block — for a visual layer of your own stylesheets rather than the built-in ones. It also keeps the built-in keys out of autocompletion, so nothing suggests Tokens your stylesheets never read.
Teach the editor your structure with InferThemeTokens — the type augmentation technique, derived from the Theme itself. Declaring it in the Theme's module means importing the Theme brings the typing along:
import { defineTheme, type InferThemeTokens } from '@vueform/ui'
export const visual = defineTheme({
surface: '#ffffff',
accent: '#f59e0b',
dark: {
surface: '#111827',
},
})
declare module '@vueform/core' {
interface ThemeTokens extends InferThemeTokens<typeof visual> {}
}This is the same augmentation the @vueform/ui/themes entry performs for the built-in Token structure.
TIP
For a Theme the built-in stylesheets consume, prefer importing defineTheme() from @vueform/ui/themes, whose completeness check guarantees no variable is left undefined.