N1 Nex1on

Ciemny motyw bez błysku: przełącznik, który pamięta wybór

Przycisk ciemnego motywu, który nie błyska bielą przy przeładowaniu: krótki skrypt w head, light-dark() i color-scheme. Cały kod, zrzuty ekranu na telefonie, tablecie i komputerze oraz sprawdzenie, że błysku nie ma.

Włączasz ciemny motyw, a przy każdym przeładowaniu strona na ułamek sekundy błyska bielą, zanim zrobi się ciemna. To najczęstszy błąd w przełączniku motywu, a naprawia go jeden malutki skrypt we właściwym miejscu. Poniżej cały przepis: kod do skopiowania, wyjaśnienie, jak działa, i sposób, by samodzielnie sprawdzić wynik.

Co otrzymasz #

  • Domyślnie motyw odpowiada motywowi systemu (jasnemu lub ciemnemu) i działa nawet przy wyłączonym JavaScripcie.
  • Przycisk przełącza motyw, a wybór jest zapamiętywany w przeglądarce.
  • Zapamiętany wybór jest stosowany przed pierwszym wyrenderowaniem: żadnego białego błysku przy przeładowaniu.
Ciemny motyw na telefonie, szerokość 390: nagłówek z przyciskiem „Dark” i karta
Ciemny motyw na telefonie, szerokość 390: nagłówek z przyciskiem „Dark” i karta
Ciemny motyw na tablecie, szerokość 768
Ciemny motyw na tablecie, szerokość 768
Ciemny motyw na komputerze, szerokość 1280: wyśrodkowana karta, nic zbędnego
Ciemny motyw na komputerze, szerokość 1280: wyśrodkowana karta, nic zbędnego

Dlaczego strona błyska #

Przeglądarka rysuje stronę w trakcie czytania HTML. Jeśli wybór użytkownika stosuje skrypt na końcu strony albo po załadowaniu, przeglądarka zdąży narysować jasną wersję, a potem ją przerysowuje. Wybór musi być znany, zanim pojawi się pierwszy piksel, więc skrypt musi stać na samym początku strony, w head.

Krok 1. Zadeklaruj, że strona ma oba motywy #

Umieść w head linię meta, a zaraz po niej malutki skrypt. Odczytuje on zapamiętany wybór i, jeśli taki istnieje, zapisuje go w atrybucie data-theme elementu głównego:
headHTML
<meta name="color-scheme" content="light dark">
<script>
(function () {
  var saved = null;
  try { saved = localStorage.getItem('theme'); } catch (e) {}
  if (saved === 'light' || saved === 'dark') {
    document.documentElement.dataset.theme = saved;
  }
})();
</script>
Skrypt jest krótki i nie czeka na załadowanie strony. Blok try jest potrzebny, bo w trybie prywatnym lub przy zablokowanej pamięci przeglądarka zgłasza błąd, a bez niego cała strona zostałaby bez motywu.

Krok 2. Opisz kolory raz #

Kolory każdego motywu to para wartości w funkcji light-dark(): pierwsza dla jasnego motywu, druga dla ciemnego. Właściwość color-scheme wybiera odpowiednią:
style.cssCSS
:root {
  color-scheme: light dark;
  --bg: light-dark(#ffffff, #14161a);
  --text: light-dark(#1b1f24, #e6e8eb);
  --card: light-dark(#f3f5f8, #1e2228);
  --line: light-dark(#d8dee6, #2c333c);
  --accent: light-dark(#1a56db, #7aa7ff);
}
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
body { background: var(--bg); color: var(--text); }
Dopóki nie ma data-theme, działa color-scheme: light dark: strona przyjmuje motyw systemu bez jednej linijki JavaScriptu. Gdy skrypt zapisze wybór użytkownika, dwie ostatnie linie wymuszają właściwy motyw.

Krok 3. Dodaj przycisk #

bodyHTML
<button id="theme-toggle" type="button" aria-pressed="false"></button>
Przycisk z dwoma stanami jest prostszy niż z trzema („jasny, ciemny, jak w systemie”): od razu widać, co jest wciśnięte, i nie trzeba tłumaczyć trzeciej opcji. Dopóki ktoś go nie naciśnie, strona i tak podąża za systemem.
theme.jsJavaScript
(function () {
  var root = document.documentElement;
  var button = document.getElementById('theme-toggle');
  var names = { dark: 'Dark', light: 'Light' };

  function current() {
    return root.dataset.theme ||
      (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  }
  function show() {
    var now = current();
    button.textContent = names[now];
    button.setAttribute('aria-pressed', now === 'dark');
  }

  button.addEventListener('click', function () {
    var next = current() === 'dark' ? 'light' : 'dark';
    root.dataset.theme = next;
    try { localStorage.setItem('theme', next); } catch (e) {}
    show();
  });
  show();
})();
Ten skrypt można załadować na końcu strony: nie musi działać przed pierwszym wyrenderowaniem, tylko podpisuje przycisk i zapisuje wybór.

Sprawdź sam #

Przetestowaliśmy przepis w przeglądarce na stronie z wolnym skryptem (dwie sekundy) przed końcem, z wyborem „dark” zapisanym w pamięci i jasnym motywem systemu. 0,8 sekundy od rozpoczęcia ładowania: ze skryptem w head strona jest już ciemna, ze skryptem na końcu jest biała i taka zostaje, dopóki strona nie zostanie przeczytana do końca. Oto co widzi człowiek w tym momencie:
Skrypt na końcu strony: wybór „dark” jest zapisany, ale strona jest jasna, to właśnie błysk
Skrypt na końcu strony: wybór „dark” jest zapisany, ale strona jest jasna, to właśnie błysk
Skrypt w head: ten sam moment ładowania, strona od razu jest ciemna
Skrypt w head: ten sam moment ładowania, strona od razu jest ciemna

Sprawdziliśmy też resztę: bez JavaScriptu strona przyjmuje motyw systemu, po naciśnięciu przycisku i przeładowaniu wybór zostaje, a przy 390, 768 i 1280 punktach nie ma poziomego przewijania.
Możesz to powtórzyć tak:

  1. Otwórz stronę, naciśnij przycisk i wybierz ciemny motyw.
  2. Włącz w narzędziach deweloperskich ograniczenie sieci i przeładuj stronę: tło jest ciemne od pierwszej klatki.
  3. Wyłącz JavaScript i wyczyść pamięć: strona znów podąża za motywem systemu.

Częste błędy #

  • Skrypt na końcu strony. Błysk wraca: wybór jest znany za późno.
  • Brak color-scheme w :root. Bez niego light-dark() nie wie, którą wartość wziąć.
  • Brak try wokół localStorage. W trybie prywatnym w niektórych przeglądarkach strona się psuje.
  • Motyw tylko w JavaScripcie. Bez skryptu odwiedzający nie ma ciemnego motywu, nawet jeśli włączył go w systemie. Przepis rozwiązuje to przez color-scheme.
  • Niesprawdzony kontrast. Tekst i tło powinny dawać co najmniej 4,5 do 1, zgodnie z zasadą dostępności WCAG AA. Sprawdź oba motywy, nie jeden.
Udostępnij: Telegram WhatsApp

Przeczytaj też

Dyskusja

Komentarze

Dyskusja jeszcze się nie zaczęła

Nikt jeszcze nie skomentował. Bądź pierwszy.

Zgłoś ten wpis