Theming
Three layers of tokens compiled to CSS variables, and the four ways to change one.
A theme is data. A preset is a tree of tokens in three layers, and the engine compiles it to CSS variables under the --vt- prefix. Nothing re-renders when it changes: the variables are rewritten and the browser repaints.
The three layers
// primitive: palettes, radii, the raw material
{ blue: { 500: '#3b82f6' }, borderRadius: { md: '4px' } }
// semantic: what the application means
{ primary: { color: '{blue.500}' },
formField: { borderColor: '{surface.300}' },
colorScheme: { dark: { formField: { borderColor: '{surface.700}' } } } }
// component: what one control needs
{ button: { root: { paddingX: '0.875rem', borderRadius: '{borderRadius.md}' } } } A value in braces is a reference, and it survives compilation as one: '{primary.color}' becomes var(--vt-primary-color), not the colour it currently holds. That is what makes a single setPrimary() re-colour every button, tag, focus ring and chart series at once.
/* '{primary.color}' becomes a reference, not a copy */
--vt-primary-color: #3b82f6;
--vt-button-padding-x: 0.875rem;
--vt-button-border-radius: var(--vt-border-radius-md);Naming
- A component's token name is its lower-case name with no dashes:
ToggleSwitch→toggleswitch. - A
rootsegment is dropped:button.root.paddingX→--vt-button-padding-x. - Anything that differs between schemes goes under
colorScheme: { light, dark }at any depth.
Extending a preset
import { definePreset, palette, Prism } from '@vitral/vue';
export const Brand = definePreset(Prism, {
semantic: {
primary: palette('#7c3aed'),
formField: { borderRadius: '2px' }
},
components: {
button: { root: { borderRadius: '999px' } }
}
});definePreset deep-merges into a base, so a brand preset states only its differences. palette('#7c3aed') derives the eleven shades from one colour.
At runtime
import { useTheme } from '@vitral/vue';
const { setPreset, setColorScheme, toggleDark, setPrimary, setSurface, setBorders, isDark } = useTheme();
setPrimary('{emerald}'); // a palette reference
setPrimary('#7c3aed'); // or a hex value, and the shades are derived
setSurface({ dark: '{slate}' }); // the greys, per scheme
setColorScheme('system'); // light, dark, or whatever the OS says
setBorders('strong'); // the edges that meet WCAG 1.4.11Two sets of edges
A preset draws its borders the way it wants to look, which in most of them is quieter than WCAG 1.4.11 asks of a control's boundary. The stronger set lives in the preset under strongBorders, compiles into a block of its own, and is chosen with borders: 'strong' when the plugin is installed or setBorders afterwards. Nothing is recompiled either way — it is an attribute on <html>, so the switch is instant and a preset of your own can carry its own pair.
export const Brand = definePreset(Ink, {
semantic: { colorScheme: { light: { formField: { borderColor: '{surface.200}' } } } },
// Only what changes when the stronger edges are asked for.
strongBorders: {
colorScheme: {
light: { formField: { borderColor: 'color-mix(in srgb, {text.color} 40%, {surface.200})' } },
dark: { formField: { borderColor: 'color-mix(in srgb, {text.color} 26%, {surface.800})' } }
}
}
});
app.use(Vitral, { theme: { preset: Brand, borders: 'strong' } });One instance at a time
dt writes the same tokens on one component's root element, so a single control can differ without a preset for it. Below that, plain CSS variables work as they always did.
<!-- one instance, one token -->
<Button label="Square" :dt="{ button: { borderRadius: '0' } }" />
<!-- or a whole subtree, with the CSS variables directly -->
<div style="--vt-primary-color: tomato">
<Button label="Tomato" />
</div>Scope and cascade
Component CSS is injected once per component, in document order, and can be wrapped in a cascade layer with the cssLayer option. With it, any application rule wins on specificity alone and !important stays out of your stylesheet.