Тёмная тема на веб-стандартах
- title
- Тёмная тема на веб-стандартах
- type
- summary
- summary
- Почему переключателю тёмной темы не подходит prefers-color-scheme и что использовать взамен
- tags
- web-standards, css, frontend
- sources
- dark-mode-web-standards
- created
- 2026-07-23
- updated
- 2026-07-29
- lang
- ru
- translation_of
- dark-mode-web-standards
- source_updated
- 2026-07-29
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Олли Уильямс разбирает, как реализовать тёмную тему без фреймворков и без class="dark" на корневом элементе, а также где веб-платформа всё ещё сопротивляется. Задачу определяют два требования: учитывать настройки ОС для нового посетителя и позволять ему переопределять тему на конкретном сайте. Пользователь вполне может предпочитать тёмный интерфейс приложений, но светлый фон для чтения длинных текстов.
Метатег вместо медиавыражения
Цветовую схему документа можно объявить двумя способами - через CSS-свойство color-scheme на html или через тег <meta name="color-scheme"> в head. Уильямс рекомендует метатег: при плохом соединении CSS может загружаться долго, а схему нужно определить до первой отрисовки. По умолчанию отдаётся content="light dark", чтобы следовать настройкам ОС, а переключатель перезаписывает атрибут на light, dark или обратно на light dark, сохраняя выбор в localStorage:
const metaTag = document.querySelector('[name="color-scheme"]');
const savedScheme = localStorage.getItem("colorScheme");
if (savedScheme) { metaTag.setAttribute('content', savedScheme); }
btndark.addEventListener('click', function() {
metaTag.setAttribute('content', 'dark');
localStorage.setItem("colorScheme", "dark");
});
btnsystem.addEventListener('click', function() {
metaTag.setAttribute('content', 'light dark');
localStorage.removeItem("colorScheme");
});
Установка схемы управляет цветами, градиентами и изображениями, заданными через light-dark(), системными цветами вроде Canvas и CanvasText, полосами прокрутки, стандартным отображением элементов форм и кнопок, iframe'ами, документы в которых запросили схему через собственный метатег, а также SVG, использующими light-dark() или prefers-color-scheme.
Разрыв между color-scheme и prefers-color-scheme
prefers-color-scheme сообщает настройки ОС и полностью игнорирует color-scheme. В этом и заключается вся проблема с переключателем на странице: любые стили, спрятанные за медиавыражением, продолжают следовать за операционной системой, тогда как всё остальное реагирует на нажатую пользователем кнопку.
Главная жертва здесь - арт-дирекшн. Такая разметка не реагирует на переключатель, а эквивалента на базе color-scheme нет, если не переносить изображение в background-image:
<picture>
<source srcset="logo-dark.png" media="(prefers-color-scheme: dark)" />
<img src="logo-light.png" alt="Product logo" />
</picture>
Исключением служат два контекста, где color-scheme родительского документа всё же доходит до медиавыражения потомка: iframe'ы и SVG. Конструкция <iframe style="color-scheme: dark;"> применяет правила @media (prefers-color-scheme: dark) своего документа; то же самое справедливо для <img>, указывающего на SVG с собственным блоком <style>.
Рабочая группа CSS (CSSWG) приняла резолюцию о том, чтобы prefers-color-scheme отражал объявленную схему документа во всех контекстах (csswg-drafts PR 13857 по резолюции в issue 13377). Ни один браузер пока этого не реализовал; в статье есть ссылка на тикет в Chromium для тех, кто хочет за него проголосовать.
Состояние Safari на момент Safari 27: prefers-color-scheme внутри SVG уже работает, но color-scheme его не переопределяет (баг WebKit 316640). Внутри iframe'ов всё работает, и родительский color-scheme переопределяет его корректно, хотя другие баги остаются открытыми (баг WebKit 316680).
light-dark() не только для цветов
Изначально функция light-dark() поддерживала только цвета, но теперь принимает также градиенты и изображения; это уже работает в Chrome и Edge 150, Firefox 150 и Safari Technology Preview.
.bg-gradient {
background-image: light-dark(linear-gradient(15deg, #b9b6ff, #308dc6), linear-gradient(15deg, #6b7495, #001339));
}
.bg {
background-image: light-dark(url(/lightmode.avif), url(/darkmode.avif));
}
Градиент можно даже заменить на сплошной цвет, обернув цвет в image().
Изменение свойств, не связанных с цветом
Иногда разница между темами заключается не в значении цвета. Тень box-shadow, которая хорошо видна на белом, исчезает на тёмном фоне, и там уместнее использовать рамку. Сейчас в CSS нет способа напрямую спросить "какая схема активна". CSSWG обсуждает конструкцию if() или стилевой запрос (style query) для этого, но пока ничего не реализовано.
Сейчас работают два обходных пути. Первый выставляет custom property на основе значения метатега и опрашивает его:
html { --dark: false; }
html:has([content="light dark"]) {
@media (prefers-color-scheme: dark) { --dark: true; }
}
html:has([content="dark"]) { --dark: true; }
@container style(--dark: true) {
.card { border: solid 1px rgb(94, 94, 94); }
}
Уильямс предпочитает второй вариант: регистрируется свойство с типом <color>, ему присваивается значение через light-dark(), а затем вычисленное значение проверяется через style query - так схему разрешает сам браузер, и таблице стилей не нужно вычислять её заново:
@property --usedScheme {
syntax: "<color>";
inherits: true;
initial-value: transparent;
}
body { --usedScheme: light-dark(white, black); }
@container style(--usedScheme: black) {
.card { border: solid 1px rgb(94, 94, 94); }
}
Стилевые запросы сейчас поддерживаются во всех браузерах, что и делает оба варианта жизнеспособными.
Что может появиться позже
Существует черновой вариант переопределения prefers-color-scheme из JavaScript - раздел script-control-user-prefs в Media Queries 5, статья на MDN про User Preferences API и прототип в Chrome Canary. Команда Safari публично выступает против этой идеи, поэтому прямо сейчас стоит опираться на подход с метатегом.
Объявление color-scheme также фигурирует на базовом уровне в specification-website-checklist, где приводится более узкая и практическая причина: оно предотвращает белую вспышку до загрузки CSS тёмной темы.
В misago-react-to-htmx тот же подход применён в масштабе целого приложения - серверный рендеринг островков на htmx вместо клиентского фреймворка, с конкретными цифрами размера бандла, показывающими выигрыш.