Dark mode na webu: CSS proměnné, přepínač a žádné bliknutí
Jak udělat tmavý režim správně: CSS proměnné, prefers-color-scheme, color-scheme, light-dark(), přepínač se třemi stavy a ochrana proti bliknutí bílé.
Tmavý režim dnes uživatelé očekávají. Udělat ho „nějak” je snadné, udělat ho dobře chce pár detailů: respektovat systémové nastavení, nechat uživatele přepnout ručně, nebliknout bílou při načtení a nezapomenout na formulářové prvky a scrollbary.
1. Barvy jako CSS proměnné
Základ je nepoužívat barvy napřímo, ale přes sémantické proměnné:
:root {
--bg: #ffffff;
--surface: #f4f4f5;
--text: #18181b;
--text-muted: #52525b;
--border: #e4e4e7;
--accent: #2563eb;
}
body {
background: var(--bg);
color: var(--text);
}
.card {
background: var(--surface);
border: 1px solid var(--border);
}
Pojmenovávejte proměnné podle účelu (--surface, --text-muted), ne podle barvy (--gray-100). Tmavé téma pak jen přepíše hodnoty.
2. Respektujte systémové nastavení
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0f12;
--surface: #1c1c21;
--text: #e4e4e7;
--text-muted: #a1a1aa;
--border: #2e2e35;
--accent: #60a5fa;
}
}
Tipy pro tmavou paletu:
- Nepoužívejte čistou černou
#000s čistě bílým textem – kontrast je ostrý a únavný. Tmavě šedé pozadí a lehce tlumená bílá jsou příjemnější. - Hloubku vyjadřujte světlejším povrchem, ne stínem – stíny na tmavém pozadí skoro nejsou vidět.
- Sytě barevné akcenty zesvětlete, jinak mají na tmavém pozadí nízký kontrast.
- Kontrast textu ověřte i v tmavém režimu (min. 4,5 : 1).
3. color-scheme: formuláře a scrollbary
:root { color-scheme: light dark; }
Tím prohlížeči řeknete, že stránka podporuje obě schémata. Nativní prvky – inputy, selecty, checkboxy, scrollbary – se pak automaticky přizpůsobí. Bez toho budete mít tmavou stránku s jasně bílými poli formuláře.
Totéž do HTML, aby prohlížeč věděl o schématu ještě před načtením CSS:
<meta name="color-scheme" content="light dark">
4. Funkce light-dark()
Moderní prohlížeče (od roku 2024) podporují kratší zápis, který vybere hodnotu podle aktivního color-scheme:
:root {
color-scheme: light dark;
--bg: light-dark(#ffffff, #0f0f12);
--text: light-dark(#18181b, #e4e4e7);
}
Výhoda: obě hodnoty jsou vedle sebe. Funguje jen pro barvy a vyžaduje nastavené color-scheme.
5. Ruční přepínač se třemi stavy
Ideální přepínač má tři volby: Světlý / Tmavý / Podle systému. Volbu uložíme a projevíme atributem na <html>:
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
/* tmavé hodnoty: buď systém (bez ruční volby), nebo ruční volba */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0f0f12;
--text: #e4e4e7;
/* … */
}
}
:root[data-theme="dark"] {
--bg: #0f0f12;
--text: #e4e4e7;
/* … */
}
Pokud používáte light-dark(), stačí měnit color-scheme jako výše – žádné duplikování hodnot.
<select id="theme-select" aria-label="Barevný režim">
<option value="system">Podle systému</option>
<option value="light">Světlý</option>
<option value="dark">Tmavý</option>
</select>
const select = document.getElementById('theme-select');
function applyTheme(theme) {
if (theme === 'system') {
delete document.documentElement.dataset.theme;
} else {
document.documentElement.dataset.theme = theme;
}
}
function saveTheme(theme) {
try { localStorage.setItem('theme', theme); } catch {}
}
select.value = (() => {
try { return localStorage.getItem('theme') ?? 'system'; } catch { return 'system'; }
})();
select.addEventListener('change', () => {
applyTheme(select.value);
saveTheme(select.value);
});
Přístup k localStorage je v try/catch, protože v některých režimech prohlížeče (blokované cookies, privátní okno) může vyhodit výjimku.
6. Zabraňte bliknutí bílé (FOUC)
Pokud téma aplikujete až skriptem na konci stránky, prohlížeč nejprve vykreslí světlou verzi a pak přepne. Řešení je malý blokující inline skript v <head>, ještě před CSS:
<head>
<script>
try {
const t = localStorage.getItem('theme');
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
} catch {}
</script>
<link rel="stylesheet" href="/style.css">
</head>
Je to jedna z mála situací, kdy je blokující skript v hlavičce správná volba – je malý a musí proběhnout před prvním vykreslením. Pokud máte Content Security Policy, povolte ho přes hash nebo nonce.
U serverově renderovaných aplikací (Next.js apod.) může nastat nesoulad mezi HTML ze serveru a klientem. V Reactu přidejte na <html> atribut suppressHydrationWarning, nebo použijte hotovou knihovnu jako next-themes, která tohle řeší.
Alternativa: uložte volbu do cookie a server rovnou vygeneruje <html data-theme="dark">. Pak skript nepotřebujete vůbec.
7. Obrázky, loga a ikony
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="logo-light.svg" alt="Firma s.r.o.">
</picture>
Pozor, media v <picture> reaguje jen na systémové nastavení, ne na ruční přepínač. Pro ten je jednodušší SVG ikony s fill="currentColor", které přeberou barvu textu, nebo přepínání dvou obrázků v CSS podle [data-theme].
Fotografie v tmavém režimu mohou oslňovat; jemné ztlumení pomůže:
:root[data-theme="dark"] img:not([src$=".svg"]) { filter: brightness(0.9); }
8. Plynulý přechod (volitelně)
body { transition: background-color 200ms, color 200ms; }
Nepřidávejte přechod na * – při přepnutí by se animovaly stovky prvků a stránka by se zasekla.
Test
- DevTools → Rendering → Emulate CSS media feature prefers-color-scheme – přepínání bez změny systému.
- Zkontrolujte formuláře,
<code>bloky, tabulky, stíny a obsah z iframe (mapy, videa). - Načtěte stránku v tmavém režimu s pomalým připojením (Network → Slow 4G) a hledejte bliknutí.