Dark mode
Aidash Components is dark-mode-first — every token has a paired light and dark value. Toggling is one class on <html>. This guide shows the wiring, the pitfalls, and the small polish steps that make the switch feel right.
.dark class rather than using a media query, so users can override the OS preference. The class strategy also plays nicely with SSR.Setup
Four steps from a light-only build to a proper light-and-dark app.
Split your tokens
Declare every semantic token twice — once inside :root (light) and once inside .dark.
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.92 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.18 0 0);
--muted: oklch(0.22 0 0);
--muted-foreground: oklch(0.68 0 0);
--border: oklch(0.26 0 0);
}Prevent the flash of unstyled content
Inline a tiny script inside <head> so the .dark class lands before the first paint. Without it, users get a white flash before the dark theme applies.
<script dangerouslySetInnerHTML={{ __html: `
(function(){
try {
var t = localStorage.getItem('theme');
if (t === 'dark' || (t !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
})();
` }} />Build a toggle
A tiny client component that reads and writes localStorage plus the <html> class.
'use client';
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const [dark, setDark] = useState<boolean>(false);
useEffect(() => {
setDark(document.documentElement.classList.contains('dark'));
}, []);
function toggle() {
const next = !dark;
setDark(next);
document.documentElement.classList.toggle('dark', next);
localStorage.setItem('theme', next ? 'dark' : 'light');
}
return (
<button
onClick={toggle}
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{dark ? '🌞' : '🌙'}
</button>
);
}Sync with the OS
If the user hasn't made a choice, follow the system preference and update it live as they flip their OS setting.
useEffect(() => {
const stored = localStorage.getItem('theme');
if (stored) return; // user preference wins
const media = window.matchMedia('(prefers-color-scheme: dark)');
const apply = () => document.documentElement.classList.toggle('dark', media.matches);
apply();
media.addEventListener('change', apply);
return () => media.removeEventListener('change', apply);
}, []);Best practices
Four rules of thumb from years of building interfaces that read well in both modes.
Never use pure black
oklch(0.10 0 0) reads softer than #000. Pure black plus text creates painful contrast on OLED screens.
Reference tokens, not hex
Read --background, --foreground, --border. Every component in Aidash does — flipping the class is enough.
Watch saturated hues
A bright accent that looks great on white can "vibrate" on black. Drop chroma 10–20% for dark contexts.
Test both continuously
Do not "add dark mode later" — build in both modes at the same time. Bugs compound if you don't.
Detecting the mode from JS
Sometimes you need to render different content based on theme (e.g. swap a logo).
import { useEffect, useState } from 'react';
export function useTheme() {
const [dark, setDark] = useState(false);
useEffect(() => {
setDark(document.documentElement.classList.contains('dark'));
const observer = new MutationObserver(() => {
setDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, []);
return dark ? 'dark' : 'light';
}