Interface and effects
Theme and palette
Two complementary systems: the theme, which switches between light and dark, and the palette, which generates every colour of the interface from a handful of base colours. Both live in CSS variables, so your own CSS comes along without any effort.
Light and dark theme
The theme is applied on the root element, with the data-theme attribute:
<html data-theme="dark">
When the user has never chosen, the attribute is absent and the system preference holds, read
through prefers-color-scheme.
<div>
<button v-theme-toggle>Switch the theme</button>
<p v-show="$theme.resolved === 'dark'">You are on the dark theme.</p>
<p v-show="$theme.resolved === 'light'">You are on the light theme.</p>
</div>
The button receives aria-pressed, a descriptive aria-label and
data-v-theme with the theme in use.
V.theme.current; // 'light', 'dark' or 'system'
V.theme.resolved; // 'light' or 'dark', with 'system' already resolved
V.theme.set('dark');
V.theme.set('system');
V.theme.toggle(); // returns the theme it applied
The choice lives in localStorage, under the key voodoo:theme, and the
browser builds apply it before the first render. That is why the page does not open white for
someone who chose the dark theme.
Writing CSS that follows the theme
.my-box {
background: var(--v-surface);
color: var(--v-text);
border: 1px solid var(--v-border);
border-radius: var(--v-radius);
box-shadow: var(--v-shadow);
}
Because the variables already change with the theme, you do not have to write two versions of the same rule.
The palette
V.palette() generates, from a handful of base colours, the full scale of shades from
50 to 900, the dark-theme version, and the text colour with the best contrast over each colour,
all written as CSS variables on :root.
The maths happens in OKLCH, a perceptually uniform space: steps with the same difference in lightness look equally far apart to the eye, which is not the case in HSL. The text colour uses the real WCAG relative-luminance formula, so the result is always readable.
V.palette({ primary: '#6D3BF5', accent: '#FF3D8B', radius: '12px', font: 'Inter' });
V.palette({ preset: 'oceano' });
| Option | What it does |
|---|---|
preset | The starting point. The colours you pass override it |
primary, accent, success, warning, danger, info | Base colours |
neutral | The colour that tints backgrounds, text and borders. Default: the hue of the primary |
radius | Border radius, such as 12px or 0.75rem |
font | The main family. The page is still responsible for loading the font |
monoFont | The monospaced family, used by VCodeBlock |
persist | Saves the choice in localStorage. Default true |
Presets
| Name | Primary | Accent |
|---|---|---|
violeta | #6D3BF5 | #FF3D8B |
oceano | #0E7BC4 | #0FB5C9 |
floresta | #1F8A4C | #7FA80E |
poente | #E4632A | #D62F63 |
grafite | #4C5A70 | #2E7FD1 |
All of them have their contrast verified in both themes.
<div>
<script>
V.data({ usePalette: (name) => V.palette.use(name) });
</script>
<div v-data="{ presets: ['violeta', 'oceano', 'floresta', 'poente', 'grafite'] }">
<div class="linha">
<button v-for="p in presets" @click="usePalette(p)">{ p }</button>
</div>
<VButton variant="primary">A primary button</VButton>
<VBadge tone="success">Success</VBadge>
<VAlert tone="info" title="Notice">The whole palette changes at once.</VAlert>
</div>
</div>
V.palette.use('floresta'); // changes only the preset, keeping radius and font
V.palette.names; // ['violeta', 'oceano', 'floresta', 'poente', 'grafite']
V.palette.reset(); // goes back to the default and erases the saved choice
V.palette.current; // the palette in use, with all its scales
CSS tokens
These always exist, with a built-in default value, even without calling
V.palette():
| Token | What it is for |
|---|---|
--v-primary, --v-primary-hover, --v-primary-contrast | The main colour |
--v-accent | The accent colour |
--v-success, --v-warning, --v-danger, --v-info | States |
--v-surface, --v-surface-2 | Backgrounds |
--v-text, --v-text-muted | Text |
--v-border | Borders |
--v-radius, --v-radius-sm | Radii |
--v-shadow | Shadow |
--v-ease | The transition curve |
--v-z-modal, --v-z-drawer, --v-z-dropdown, --v-z-toast, --v-z-tooltip | Layers |
After V.palette(), the set grows with intermediate backgrounds, a smaller and a
larger shadow, larger radii, the focus ring, the type families and the 50-to-900 scales of every
base colour. The full list is in the
configuration reference.
Derived colours, and checking contrast
V.palette.scale('#6D3BF5')['700']; // a dark shade of the scale
V.palette.scale('#6D3BF5', true); // the dark-theme scale
V.palette.contrastText('#6D3BF5'); // '#fff' or '#000', whichever reads better
V.palette.contrastRatio('#6D3BF5', '#fff'); // the WCAG ratio, from 1 to 21
V.palette.luminance('#6D3BF5'); // relative luminance
contrastRatio gives you the real ratio: 4.5 is the WCAG AA minimum for normal text
and 3 for large text. Checking a colour in the console is faster than finding the problem later,
in an accessibility audit.
Turning off the injected CSS
<script src="voodoo.full.min.js?v=0.6.2" data-no-styles defer></script>
No style is injected. The components go on working, but with no appearance, and writing the rules
for the v-* classes is up to you. If you use Tailwind, the more comfortable road is
usually the opposite: keep the injected CSS and point Tailwind's colours at the
--v-* variables, so that the two systems move together.