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
No flash

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' });
OptionWhat it does
presetThe starting point. The colours you pass override it
primary, accent, success, warning, danger, infoBase colours
neutralThe colour that tints backgrounds, text and borders. Default: the hue of the primary
radiusBorder radius, such as 12px or 0.75rem
fontThe main family. The page is still responsible for loading the font
monoFontThe monospaced family, used by VCodeBlock
persistSaves the choice in localStorage. Default true

Presets

NamePrimaryAccent
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():

TokenWhat it is for
--v-primary, --v-primary-hover, --v-primary-contrastThe main colour
--v-accentThe accent colour
--v-success, --v-warning, --v-danger, --v-infoStates
--v-surface, --v-surface-2Backgrounds
--v-text, --v-text-mutedText
--v-borderBorders
--v-radius, --v-radius-smRadii
--v-shadowShadow
--v-easeThe transition curve
--v-z-modal, --v-z-drawer, --v-z-dropdown, --v-z-toast, --v-z-tooltipLayers

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
Use it while developing

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.