Tryb ciemny to już nie fanaberia

Jeszcze kilka lat temu dark mode był ciekawostką dostępną głównie w edytorach kodu. Dziś to standard — systemy operacyjne, przeglądarki i większość popularnych aplikacji oferują tryb ciemny. Użytkownicy go oczekują, a poprawna implementacja wpływa na komfort korzystania ze strony, zwłaszcza wieczorem i w nocy.

Jak zaimplementować dark mode poprawnie, bez bałaganu w kodzie i z poszanowaniem preferencji użytkownika? Oto nasz sprawdzony przepis.

Krok 1: CSS Custom Properties jako fundament

Podstawą elastycznego systemu kolorów są zmienne CSS (CSS Custom Properties). Zamiast hardkodować kolory w dziesiątkach miejsc, definiujemy je raz i używamy w całym arkuszu stylów.

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a2e;
  --color-heading: #16213e;
  --color-accent: #0f3460;
  --color-surface: #f5f5f5;
  --color-border: #e0e0e0;
}

Następnie używamy tych zmiennych zamiast bezpośrednich wartości kolorów:

body {
  background-color: var(--color-bg);
  color: var(--color-text);
}

h1, h2, h3 {
  color: var(--color-heading);
}

Ta struktura sprawia, że zmiana motywu sprowadza się do nadpisania kilku zmiennych — nie trzeba duplikować reguł CSS.

Krok 2: Automatyczne wykrywanie preferencji systemowych

Media query prefers-color-scheme pozwala wykryć, czy użytkownik ma włączony tryb ciemny na poziomie systemu operacyjnego:

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1a1a2e;
    --color-text: #e0e0e0;
    --color-heading: #eaeaea;
    --color-accent: #4fc3f7;
    --color-surface: #16213e;
    --color-border: #2a2a4a;
  }
}

To rozwiązanie działa automatycznie — bez żadnego JavaScript. Jeśli użytkownik ma w systemie włączony dark mode, strona od razu wyświetli się w ciemnym motywie. To powinien być absolutne minimum każdej implementacji.

Krok 3: Przełącznik ręczny

Automatyczne wykrywanie to świetny start, ale użytkownicy cenią sobie możliwość ręcznego przełączania. Niektórzy wolą dark mode w przeglądarce, choć system mają w trybie jasnym.

Najprostsze podejście polega na dodaniu klasy do elementu <html>:

html[data-theme="dark"] {
  --color-bg: #1a1a2e;
  --color-text: #e0e0e0;
  --color-heading: #eaeaea;
  --color-accent: #4fc3f7;
  --color-surface: #16213e;
  --color-border: #2a2a4a;
}

A obsługa przełącznika w JavaScript:

const toggle = document.getElementById('theme-toggle');

toggle.addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';
  document.documentElement.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
});

Krok 4: Zapisywanie i odtwarzanie preferencji

Kluczowe jest, aby wybór użytkownika przetrwał odświeżenie strony. Używamy do tego localStorage:

function getPreferredTheme() {
  const saved = localStorage.getItem('theme');
  if (saved) return saved;

  return window.matchMedia('(prefers-color-scheme: dark)').matches
    ? 'dark'
    : 'light';
}

document.documentElement.setAttribute('data-theme', getPreferredTheme());

Ważne: ten skrypt powinien być umieszczony w <head> strony, najlepiej jako inline <script>, a nie jako zewnętrzny plik. Dlaczego? Aby uniknąć efektu flashowania (FOUC — Flash of Unstyled Content), gdzie strona na ułamek sekundy wyświetla się w jasnym motywie, zanim JavaScript zmieni schemat kolorów.

Krok 5: Reagowanie na zmiany systemowe

Użytkownik może zmienić motyw systemowy w trakcie przeglądania strony. Warto na to reagować — ale tylko wtedy, gdy nie nadpisał preferencji ręcznie:

window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    if (!localStorage.getItem('theme')) {
      document.documentElement.setAttribute(
        'data-theme',
        e.matches ? 'dark' : 'light'
      );
    }
  });

Najczęstsze błędy przy implementacji dark mode

Na podstawie naszych doświadczeń zebraliśmy listę pułapek, w które łatwo wpaść:

  • Za niski kontrast tekstu — ciemne tło nie oznacza, że tekst może być szary. Minimalny współczynnik kontrastu to 4.5:1 dla normalnego tekstu (standard WCAG AA). Narzędzie takie jak WebAIM Contrast Checker pomoże to zweryfikować
  • Czysto biały tekst na czarnym tle — to zbyt agresywne dla oczu. Lepiej użyć lekkiego odcienia szarości (#e0e0e0) na ciemnogranatowym tle (#1a1a2e)
  • Zapominanie o obrazkach — zdjęcia i grafiki mogą wyglądać dziwnie na ciemnym tle. Rozważ dodanie delikatnego zaokrąglonego obramowania lub cienia
  • Brak testowania formularzy — pola input, selecty i przyciski często wymagają osobnych stylów dla dark mode
  • Hardkodowane kolory w komponentach — jeden zapomniany color: #333 w zagnieżdżonym komponencie potrafi zepsuć cały motyw

Obrazki i multimedia

Dla grafik SVG osadzonych inline zmiana kolorów jest prosta — wystarczy użyć tych samych zmiennych CSS. Dla bitmap (PNG, JPG) warto rozważyć:

@media (prefers-color-scheme: dark) {
  img.invertible {
    filter: brightness(0.9) contrast(1.1);
  }

  img.logo-light {
    display: none;
  }

  img.logo-dark {
    display: block;
  }
}

Alternatywnie, element <picture> pozwala serwować różne wersje obrazka w zależności od motywu:

<picture>
  <source srcset="logo-dark.png"
          media="(prefers-color-scheme: dark)">
  <img src="logo-light.png" alt="Logo">
</picture>

Podsumowanie

Prawidłowa implementacja dark mode to nie jest kwestia jednego media query. To przemyślany system kolorów oparty na zmiennych CSS, automatyczne wykrywanie preferencji systemowych, ręczny przełącznik z zapisem do localStorage i eliminacja flashowania przy ładowaniu strony.

Zacznij od zdefiniowania pełnej palety kolorów w zmiennych CSS, dodaj media query prefers-color-scheme, a dopiero potem dobuduj przełącznik ręczny. Testuj w obu trybach na każdym etapie pracy — a Twoi użytkownicy będą wdzięczni za komfortowe przeglądanie strony o każdej porze dnia.

Potrzebujesz pomocy z implementacją dark mode lub innego elementu frontendu? Skontaktuj się z nami.