Dark mode toggle
A toggle button that switches the page between light and dark themes, remembers the choice and follows the operating system until a choice is made.
Preview
Appearance
Follows your system setting until you choose.
In this preview the button switches this panel only and stores nothing. The script below switches the whole page.
With utility classes
The markup of the preview. Classes come from the prebuilt dist/nuvoui.min.css, or from your own build with $generate-utility-classes: true.
<div class="flex wrap between x-center gap-4 bg-surface text-regular border rounded-lg p-4">
<div>
<p class="font-semibold m-0">Appearance</p>
<p class="text-sm text-muted m-0">Follows your system setting until you choose.</p>
</div>
<button class="bg-alternate text-regular border hover:bg-hover rounded-full text-sm" type="button" aria-pressed="false" data-theme-toggle>Dark mode</button>
</div>With apply()
The same result with BEM class names and no utility classes. @use "@nuvoui/core" as *; emits the tokens and base styles, and each apply() writes one rule.
@use "@nuvoui/core" as *;
.appearance {
@include apply(
flex, // display: flex
wrap, // let items wrap onto new lines
between, // main axis: space between (justify-content)
x-center, // cross axis: center (align-items)
gap(4), // gap: spacing key 4 (1rem)
p(4), // padding: spacing key 4 (1rem)
bg(surface), // surface background, follows the theme
text-color(regular), // regular text color, follows the theme
border(1), // 1px border (width key 1), regular color
rounded(lg) // corner radius: size key lg (0.75rem)
);
}
.appearance__title {
@include apply(
m(0), // margin: 0
font(semibold) // font weight: semibold (600)
);
}
.appearance__text {
@include apply(
m(0), // margin: 0
font-size(sm), // font size key sm (0.875rem)
text-color(muted) // muted text color, follows the theme
);
}
.theme-toggle {
@include apply(
rounded(full), // fully rounded ends (pill shape)
font-size(sm), // font size key sm (0.875rem)
bg(alternate), // alternate background, follows the theme
text-color(regular), // regular text color, follows the theme
border(1), // 1px border (width key 1), regular color
bg(hover) hover // hover background on hover
);
&[aria-pressed="true"] {
@include apply(
bg(selected), // selected item background
text-color(brand), // brand text color
border-color(brand) // border color: brand
);
}
}<div class="appearance">
<div>
<p class="appearance__title">Appearance</p>
<p class="appearance__text">Follows your system setting until you choose.</p>
</div>
<button class="theme-toggle" type="button" aria-pressed="false" data-theme-toggle>Dark mode</button>
</div>Script
The first script runs in <head>, so a stored theme applies before the first paint. The second switches the theme and keeps aria-pressed in step with it. The playground blocks scripts; use its theme switch to see the dark tokens there.
<!-- In <head>: applies a stored choice before the first paint -->
<script>
try {
const theme = localStorage.getItem("theme");
if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme;
} catch {}
</script>
<!-- At the end of <body> -->
<script>
(() => {
const button = document.querySelector("[data-theme-toggle]");
const root = document.documentElement;
const system = matchMedia("(prefers-color-scheme: dark)");
const isDark = () => (root.dataset.theme ? root.dataset.theme === "dark" : system.matches);
const sync = () => button.setAttribute("aria-pressed", String(isDark()));
button.addEventListener("click", () => {
const next = isDark() ? "light" : "dark";
root.dataset.theme = next;
try {
localStorage.setItem("theme", next);
} catch {}
sync();
});
system.addEventListener("change", sync);
sync();
})();
</script>Tokens used
Every color comes from a theme token, so the recipe follows the light, dark and inverse themes with no extra CSS.
data-theme- Set on the html element, it selects the light or dark token block. Without the attribute, NuvoUI follows prefers-color-scheme, so a first visit matches the operating system.
bg-surface,text-regular,text-muted- The panel and its text; they flip with the theme.
bg-alternate,bg-hover,border-regular- The button at rest and on hover.
bg-selected,text-brand,brand- The pressed state in the SCSS version, matched on aria-pressed="true".
border-focus- The focus ring the base layer draws on every button.