Theming
Colors come from CSS custom properties. Two stylesheet layers decide what your components look like:
style.css— required. Ships the component structure and the defaultlightpalette on:root, so components render correctly with nothing else imported and nodata-themeset.theme-*.css— optional. Each file defines one palette scoped under the higher-specificityhtml[data-theme="…"]selector, so it overrides the:rootdefault whenever the matchingdata-themeis set — regardless of import order.
There are four built-in themes:
data-theme | Stylesheet | |
|---|---|---|
light | theme-light.css | implicit default |
dark | theme-dark.css | |
light-neon | theme-light-neon.css | |
dark-neon | theme-dark-neon.css |
Zero-config (just light)
Import one file and you're done — light works, no data-theme needed:
import '@popovandrii/ui-elements/style.css' // that's itWhen no data-theme is set, style.css also follows the OS via @media (prefers-color-scheme: dark), so the dark palette kicks in automatically in OS dark mode. Any explicit data-theme always wins over this fallback.
Pin one explicit theme
Load the theme's file and set data-theme on <html>:
import '@popovandrii/ui-elements/style.css'
import '@popovandrii/ui-elements/theme-dark.css'<html data-theme="dark">
…
</html>Because themes are html[data-theme="…"] (higher specificity than :root), the explicit theme always wins; remove the attribute to fall back to light.
Load all + switch at runtime
Import every theme you want to offer, then flip data-theme whenever:
import '@popovandrii/ui-elements/style.css' // required (also = light default)
import '@popovandrii/ui-elements/theme-dark.css'
import '@popovandrii/ui-elements/theme-light-neon.css'
import '@popovandrii/ui-elements/theme-dark-neon.css'
// theme-light.css only needed to switch *back* to light explicitly// light | dark | light-neon | dark-neon
document.documentElement.dataset.theme = 'dark'That's all switching is — one attribute. No re-init of the components is needed; they read the CSS variables live.
A theme switcher in Vue
This is exactly what the live demos on this site do:
<script setup lang="ts">
import { ref } from 'vue'
const themes = ['light', 'dark', 'light-neon', 'dark-neon'] as const
const theme = ref<(typeof themes)[number]>('light')
function setTheme(t: (typeof themes)[number]) {
theme.value = t
document.documentElement.dataset.theme = t
}
</script>
<template>
<button
v-for="t in themes"
:key="t"
type="button"
:aria-pressed="theme === t"
@click="setTheme(t)"
>
{{ t }}
</button>
</template>Persist the choice
Save the pick (e.g. localStorage.setItem('theme', t)) and re-apply it early on load to avoid a flash of the default theme.
Optional page reset (base.css)
base.css is a small page normalize/reset (body, headings, links, form controls, tables…). It is not bundled into style.css, so importing the components never restyles your page. Import it only if you want the reset:
import '@popovandrii/ui-elements/style.css'
import '@popovandrii/ui-elements/base.css' // opt-inIt also sets a mobile-first root font-size that scales the whole rem-based system up on small screens.
Custom theme
Themes are just CSS variables, so instead of shipping a full theme file you can override individual tokens under your own selector:
html[data-theme='brand'] {
--c-p-500: #7c3aed; /* primary */
--c-g-50: #faf9ff; /* lightest gray / surface */
/* …override the --c-* tokens you care about */
}Then document.documentElement.dataset.theme = 'brand' like any built-in one.