Programování

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

12. 10. 2026 · 4 min čtení

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 #000 s č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í.