Dark mode
Class-based toggling, persisted preference, system-preference fallback, and dark-aware tokens.
Overview
Byteion uses semantic CSS variables for every color, surface, and border token. All components reference these variables — never raw hex values. When .dark is applied to <html>, the variable values swap from light to dark and every component re-renders automatically. There is no separate dark theme to maintain, no duplicated component CSS, and no dark: variant needed when using semantic tokens.
Two activation strategies are supported:
.dark class (recommended)
Add class="dark" to <html> to toggle the entire page. This is the standard app-level strategy — it works with Tailwind's darkMode: 'class', persists cleanly in localStorage, and can be set before first paint to eliminate flash.
[data-theme="dark"]
Use [data-theme="dark"] on any container for scoped dark mode — ideal for embedded widgets, documentation previews, theme demos, or design tool mockups. The Tailwind preset recognizes both selectors.
How it works
1. Variable layers
Light defaults on :root + [data-theme="light"], dark overrides on .dark + [data-theme="dark"]. Brand themes live on [data-theme="<brand>"] and can opt into dark via [data-mode="dark"].
2. Toggle selector
Add or remove class="dark" on <html>. The @custom-variant dark declaration in v4 (or darkMode: ['class', '[data-theme="dark"]'] in v3) ensures dark: utilities fire under either selector.
3. Tokens re-resolve
Every bg-background, text-foreground, and border-border reads the variable at render time. Change the selector, and every class resolves to its dark value instantly — no component code changes.
The dark block in variables.css only overrides color and surface variables. Shadows, motion, and radius tokens are not redeclared — they remain identical across modes. This keeps the dark override minimal and predictable:
:root, [data-theme="light"] {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--border: 240 5.9% 90%;
--muted-foreground: 240 3.8% 43%;
/* … 50+ variables for colors, shadows, motion, z-index, radius */
}
.dark, [data-theme="dark"] {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 6% 10%;
--border: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
/* Only color/surface variables are overridden */
}
Tailwind configuration
Dark mode works differently depending on whether you use Tailwind v3 or v4 with Byteion.
Tailwind v3
Add Byteion's preset to your tailwind.config.js. The preset already sets darkMode: ['class', '[data-theme="dark"]'] so both selectors activate dark: utilities:
module.exports = {
darkMode: ['class', '[data-theme="dark"]'],
presets: [require('@byteion/core/preset')],
content: ['./src/**/*.{html,js,jsx,tsx,vue}'],
};
Then import the token CSS in your entry stylesheet:
@import "@byteion/core/theme/generated/tokens.css";
@import "@byteion/core/theme/variables.css";
Tailwind v4
Import Byteion's v4 entry in your main CSS file. It includes the token layer, semantic variables, and the @custom-variant dark declaration — no JavaScript config needed:
@import "tailwindcss";
@import "@byteion/core";
The @custom-variant dark declaration in Byteion's theme/index.css matches both .dark and [data-theme="dark"]. This is equivalent to v3's darkMode: ['class', '[data-theme="dark"]'] but expressed entirely in CSS.
Toggle script
Persist the user's choice in localStorage and apply it before paint to avoid a flash of light content:
<!-- Inline in <head>, BEFORE any stylesheet -->
<script>
(function () {
const stored = localStorage.getItem('theme');
const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
const shouldBeDark = stored ? stored === 'dark' : prefersDark;
document.documentElement.classList.toggle('dark', shouldBeDark);
})();
</script>
Wire up a toggle button with accessible icon swap:
<button type="button" id="theme-toggle" aria-label="Toggle dark mode"
class="inline-flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted">
<i class="ti ti-moon dark:hidden"></i>
<i class="ti ti-sun hidden dark:inline-block"></i>
</button>
<script>
document.querySelector('#theme-toggle').addEventListener('click', () => {
const next = document.documentElement.classList.toggle('dark') ? 'dark' : 'light';
localStorage.setItem('theme', next);
});
</script>
Respect system preference
The toggle script above defers to prefers-color-scheme when the user has no stored preference. To also react when the OS theme changes while the page is open:
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
document.documentElement.classList.toggle('dark', e.matches);
}
});
This ensures a user who switches their OS from light to dark at 2pm sees the page adapt immediately — but only if they have not explicitly chosen a mode. A stored preference always wins over the OS signal.
Scoped dark mode
Apply .dark on a container to render that subtree in dark mode regardless of the page-level setting. This is useful for embedded previews, documentation examples, theme comparison widgets, or design tool mockups:
<!-- This card renders in dark mode even if the page is light -->
<div class="dark">
<div class="rounded-card border border-border bg-card p-lg shadow-card">
<h3 class="text-base font-semibold text-foreground">Dark preview</h3>
<p class="text-sm text-muted-foreground mt-sm">
This block uses dark token values regardless of the outer theme.
</p>
</div>
</div>
The same applies to [data-theme="dark"] — place it on any parent element to scope dark mode to that subtree. This is especially useful in documentation sites that show light and dark variants side by side.
Dark-aware tokens
When you use semantic tokens, you never need to write a dark: variant. The token resolves to the correct value for the active mode. Brand colors like --primary stay the same across modes; only surfaces and chromes swap:
| Token class | Light (:root) | Dark (.dark) |
|---|---|---|
| bg-background | 0 0% 100% | 240 10% 3.9% |
| text-foreground | 240 10% 3.9% | 0 0% 98% |
| bg-card | 0 0% 100% | 240 6% 10% |
| bg-muted | 240 4.8% 95.9% | 240 3.7% 15.9% |
| text-muted-foreground | 240 3.8% 43% | 240 5% 64.9% |
| border-border | 240 5.9% 90% | 240 3.7% 15.9% |
| ring (brand) | 217 91% 60% | 217 91% 60% |
Developer tips
dark: variants. bg-background resolves to white in light mode and near-black in dark mode. You never need bg-white dark:bg-zinc-900.
• The toggle script must run before paint. Inline it in <head> before any stylesheet to prevent a flash of light content (FOLC).
• Stored preference beats OS signal. If the user explicitly toggled dark mode, respect that choice until they change it. Only fall back to prefers-color-scheme when there is no stored value.
• Test every new screen with dark mode toggled. A component that works in light mode may have hardcoded colors or missing semantic tokens. Toggle .dark during development as part of your review workflow.
• Brand colors stay constant across modes. --primary and --ring do not change in dark mode. Only surfaces, foregrounds, borders, and muted elements swap — brand colors remain visually consistent.Do and Don't
Do
- Use semantic tokens (
bg-background,text-foreground,bg-card) instead ofbg-white dark:bg-zinc-900 - Run the toggle script in
<head>before the first paint - Test every new screen or component with
.darktoggled - Honour
prefers-color-schemefor the initial un-persisted state - Use
.darkon<html>for app-level toggling and[data-theme="dark"]for scoped previews
Don't
- Hardcode
bg-whiteorbg-zinc-900on shared surfaces — they will break in dark mode - Sprinkle
dark:variants when a semantic token already covers both modes - Set the dark class from a deferred or async script — you will get a flash of light content
- Rely on the OS preference only without offering a manual toggle
- Assume brand colors (
--primary,--ring) change in dark mode — they stay constant; only surface colors swap
Related
- Theming Guide — Full theme customization through CSS variables
- Colors — Semantic and functional color token system
- CSS Variables — Complete variable reference
- Design Tokens — Token system architecture
- Quick Start — Set up Byteion in a new project
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.
Explore Pro Components