Тёмная тема без мигания: переключатель, который запоминает выбор

Кнопка тёмной темы, которая не вспыхивает белым при перезагрузке: короткий скрипт в head, light-dark() и color-scheme. Код целиком, снимки на телефоне, планшете и компьютере и проверка, что вспышки нет.
Вы включили на сайте тёмную тему, а при каждой перезагрузке страница на долю секунды вспыхивает белым и только потом темнеет. Это самая частая ошибка переключателя темы, и лечится она одним маленьким скриптом в нужном месте. Ниже — рецепт целиком: код, который можно скопировать, объяснение, как он работает, и способ проверить результат самому.
Что получится #
- Тема по умолчанию совпадает с темой системы (светлой или тёмной) и работает даже при выключенном JavaScript.
- Кнопка переключает тему, а выбор запоминается в браузере.
- Сохранённый выбор применяется до первой отрисовки: при перезагрузке нет белой вспышки.



Почему страница мигает #
Браузер рисует страницу по мере чтения HTML. Если выбор пользователя применяет скрипт в конце страницы или после загрузки, браузер успевает нарисовать светлый вариант, а потом перекрашивает его. Нужно, чтобы выбор был известен раньше, чем появится первый пиксель, то есть скрипт должен стоять в самом начале страницы, вhead.Шаг 1. Объявите, что сайт умеет обе темы #
Положите вhead строку meta и сразу за ней крошечный скрипт. Он читает сохранённый выбор и, если он есть, записывает его в атрибут data-theme корневого элемента:
<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>try нужен потому, что в приватном режиме или при запрещённом хранилище браузер бросает ошибку, и без него вся страница осталась бы без темы.Шаг 2. Опишите цвета один раз #
Цвета каждой темы задаются парой значений в функцииlight-dark(): первое для светлой темы, второе для тёмной. Выбирает нужное свойство color-scheme:
: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); }data-theme нет, работает color-scheme: light dark: страница берёт тему системы без единой строки JavaScript. Когда скрипт записал выбор пользователя, две последние строки принудительно включают нужную тему.Шаг 3. Добавьте кнопку #
<button id="theme-toggle" type="button" aria-pressed="false"></button>(function () {
var root = document.documentElement;
var button = document.getElementById('theme-toggle');
var names = { dark: 'Тёмная', 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();
})();Проверьте сами #
Мы проверили рецепт в браузере на странице, где перед концом стоит медленный скрипт (две секунды), а в хранилище записан выбор «тёмная» при светлой теме системы. Через 0,8 секунды после начала загрузки: со скриптом вhead страница уже тёмная, со скриптом в конце — белая, и так остаётся, пока страница не дочитана. Вот что видит человек в этот момент:

Остальное тоже проверено: без JavaScript страница берёт тему системы, после нажатия кнопки и перезагрузки выбор остаётся, на 390, 768 и 1280 точек нет бокового скролла.
Повторить можно так:
- Откройте страницу, нажмите кнопку и выберите тёмную тему.
- Включите в инструментах разработчика замедление сети и перезагрузите страницу: фон остаётся тёмным с первого кадра.
- Выключите JavaScript и очистите хранилище: страница снова следует теме системы.
Типичные ошибки #
- Скрипт в конце страницы. Вспышка возвращается: выбор известен слишком поздно.
- Нет
color-schemeна:root. Функцияlight-dark()без него не знает, какое значение взять. - Нет
tryвокругlocalStorage. В приватном режиме у части браузеров страница ломается. - Тема только в JavaScript. Без скрипта посетитель остаётся без тёмной темы даже при включённой в системе. В рецепте это решено через
color-scheme. - Не проверен контраст. Текст и фон должны давать не меньше 4,5 к 1, это правило доступности WCAG AA. Проверьте обе темы, а не одну.
Пока никто не высказался. Будьте первым.