Skip to content

Theming

Colors come from CSS custom properties. Two stylesheet layers decide what your components look like:

  • style.cssrequired. Ships the component structure and the default light palette on :root, so components render correctly with nothing else imported and no data-theme set.
  • theme-*.cssoptional. Each file defines one palette scoped under the higher-specificity html[data-theme="…"] selector, so it overrides the :root default whenever the matching data-theme is set — regardless of import order.

There are four built-in themes:

data-themeStylesheet
lighttheme-light.cssimplicit default
darktheme-dark.css
light-neontheme-light-neon.css
dark-neontheme-dark-neon.css

Zero-config (just light)

Import one file and you're done — light works, no data-theme needed:

ts
import '@popovandrii/ui-elements/style.css' // that's it

When 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>:

ts
import '@popovandrii/ui-elements/style.css'
import '@popovandrii/ui-elements/theme-dark.css'
html
<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:

ts
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
ts
// 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:

vue
<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:

ts
import '@popovandrii/ui-elements/style.css'
import '@popovandrii/ui-elements/base.css' // opt-in

It 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:

css
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.