Реализация переключения цветовой схемы на сайте
Создание переключателя ночной и дневной темы, как для стилей, так и изображений.
Современные операционные системы поддерживают светлую (дневную) и тёмную (ночную) темы оформления интерфейса уже несколько лет, поддержка тем появилась и во всех основных браузерах, поэтому иметь современный сайт без поддержки различных тем просто неприлично и является неуважением к пользователю.
Соответствие настройкам текущей темы операционной системы может быть реализовано на уровне CSS автоматически, но в данной заметке будет разобран подход, который даёт возможность пользователю самостоятельно выбрать тему в любой момент.
Базовая матчасть
Медиа-запросы
Язык CSS поддерживает медиа-запросы (media queries) – это правила, которые позволяют реализовывать адаптивную вёрстку на основе тех или иных характеристик устройства пользователя. К примеру, если устройство пользователя это монитор, то можно применить одни стили, если это телевизор, то другие, а если принтер, то третьи. Можно контролировать размеры устройства, ориентацию, ну и конечно же особенности среды, в том числе и выбранную в системе (либо браузере) тему.
Ключевое слово для медиа-запроса это @media
. Фактически это обёртка для стиля: всё, что находится внутри, применяется или не применяется в зависимости от того, выполняется ли условие:
{
/* стиль будет применён только если условие истинно */
}
Чтобы узнать, какая тема используется в системе, необходимо использовать медиа-свойство prefers-color-scheme
, оно может иметь два значения:
light
– для светлого (дневного) режима. Так же данное значение используется по умолчанию.dark
– для тёмного (ночного) режима.
В качестве примера рассмотрим вот такой блок:
Светлая тема
Тёмная тема
В зависимости от темы будет отображаться только одна надпись, причем у светлой темы линия подчеркивания будет розовая, а у тёмной оранжевая:
}
}
}
{
/* Данная группа стилей будет использована
только для тёмной темы */
}
}
}
Данный подход можно использовать только для автоматического соответствия теме заданной в системе.
Медиа-запросы в JavaScript
Чтобы в JS определить, соответствует ли документ некой строке медиа-запроса, следует использовать метод matchMedia()
интерфейса window
, где в качестве единственного аргумента передается запрос. Как и с кодом CSS, это будет строка (prefers-color-scheme: <light|dark>)
. Метод возвращает объект MediaQueryList
у которого есть булево свойство matches
, оно будет равно true
если документ соответствует запросу и false
в обратном случае. Так же имеются два метода addEventListener()
и removeEventListener()
позволяющие подписаться (либо отписаться) на событие change
оповещающее об изменении состояния документа. Таким образом можно отслеживать изменение темы в системе в режиме реального времени, либо когда пользователь её меняет самостоятельно, либо когда это происходит автоматически по расписанию.
Пример:
;
Узнать тему...
Медиа-запросы для изображений
Тег <picture>
позволяет определить несколько источников (<source>
) для <img>
и с помощью медиа-запросов выбрать какой именно источник будет использоваться в конкретной ситуации. Если ни один источник не подходит, то будет использован файл по умолчанию указанный в теге <img>
.
В примере ниже используется два изображения. Первое – это дополнительный источник который будет отображаться когда медиа-запрос будет верен, в данном случае это фильтр для тёмной (dark
) темы в системе. Второе изображение – это изображение указанное в теге <img>
, оно будет использоваться по умолчанию, соответственно когда у пользователя включена светлая тема.
Данный пример работает только при переключении темы в системе (браузере), кнопка “Переключить тему” здесь лишь эмулирует подобное поведение.

Селекторы по атрибуту
HTML позволяет добавлять какие угодно пользовательские атрибуты, а CSS позволяет выборочно стилизовать такие элементы. Синтаксис в CSS довольно прост: атрибут записывается в квадратных скобках, можно указать и необходимое значение атрибута через знак равенства.
Рассмотрим пример, где двум элементам <span>
добавляется атрибут theme
, причем с различными значениями (orange
и pink
), благодаря чему к данным элементам будет применено своё уникальное правило:
}
}
}
CSS-переменные
CSS позволяет задавать переменные (пользовательские свойства) которые можно вставлять вместо конкретных значений стилевых свойств. Переменная создаётся с помощью конструкции --имя-переменной: значение
. Использование осуществляется с помошью конструкции var(--имя-переменной)
. Опционально можно указать и значение по умолчанию которое будет использовано если переменная не определена: var(--имя-переменной, значение по умолчанию)
.
}
Пользовательские свойства имеют область видимости, они существуют только в пределах указанного селектора и в наследниках попадающих под него. Однако, если задать переменную в специальном псевдоклассе :root
, то такая переменная будет считаться глобальной и доступной для любого селектора. Псевдокласс :root
по сути является аналогом html
– корневого элемента документа, но в стилях имеет ещё более высокий приоритет. Поэтому зачастую используется для хранения глобальных переменных.
В следующем примере используется переменная --size
. Её значение задается через атрибут style
непосредственно у элементов <span>
. В последнем случае ничего не задано, поэтому будет использовано значение по умолчанию.
}
Реализация
Цвета для светлой и тёмной тем будут храниться в виде пользовательских свойств в псевдоклассе :root
, что обеспечит к ним доступ из любого места. Данный пседвокласс будет определён дважды. Во втором случае с использованием селектора по пользовательскому атрибуту theme
со значением равным dark
. Таким образом, когда браузер будет рендрить документ, конкретный цвет для переменных будет определяться наличием или отсутствием данного атрибута у элемента <html>
.
В примере ниже при нажатии на кнопку js скрипт будет добавлять, либо удалять theme=dark
в тег <html>
, тем самым обеспечивая переключение темы для пользователя по его желанию.
base.css
}
}
themes.js
index.html
<!-- аттрибут theme отсутствует -->
Каждый
охотник
желает
знать,
где
сидит
фазан.
Переключить тему
Хранение пользовательского выбора
При каждом новом входе на сайт, либо открытии новых страниц, выбранная тема будет сбрасываться, поэтому необходимо каким-либо образом хранить пользовательский выбор. Для хранения подобных настроек удобно воспользоваться локальным хранилищем localStorage
. Такой подход позволяет работать исключительно на стороне клиента без обращений к серверу и избавит от необходимости постоянно передавать на него куки (cookies). Значения в localStorage
хранятся в виде пар ключ=значение
, при этом вне зависимости от того, какой тип данных используется, он всегда будет преобразован и записан как строка (string
).
Так же, при отсутствии данных о том, что выбрал пользователь (допустим при первом посещении сайта), необходимо установить тему соответствующую текущей в системе.
Ниже представлен алгоритм определения темы которую необходимо представить пользователю:

И его реализация:
themes.js
; /* медиа-запрос
соответствия тёмной теме */
; /* ключ для хранения настроек, он же
и имя атрибута для <html> */
; /* возможные названия тем */
/* Определяет, является ли текущая тема в системе "тёмной"? */
;
/* Определяет, необходимо ли пользователю показывать светлую тему? */
/* Устанавливает тему в соответствии с текущими настройками пользователя
либо системы */
/* Принудительно устанавливает для сайта заданную тему, в качестве
аргумента передается true для светлой и false для тёмной темы */
Теперь можно обновить код для кнопки, чтобы тема не просто менялась, но и выбор пользователя сохранялся между сессиями.
index.html
Переключить тему
Однако при открытии страницы тема сама по себе не будет соответствовать необходимой, поэтому следует осуществить вызов syncTheme()
. И сделать это необходимо как можно раньше. Почему раньше? Представим ситуацию, что необходимо переключить тему с той, которая загружается по умолчанию. Соответственно если вызвать syncTheme()
когда документ уже загружен хотя бы частично и браузер начал рендринг, получится заметный и неприятный для глаза переход. Допустим на секунду окно будет белым, а потом станет чёрным, либо наоборот. Чтобы не допустить подобное, вызов необходимо сделать до начала рендринга, идеальным будет размещение вызова в блоке <head>
. Для этого именно там разместим ссылку на скрипт и добавим в конце вызов syncTheme()
const setTheme = (isLight) => {
window.localStorage.setItem(STORAGE_KEY, THEME_NAME[+isLight]);
syncTheme();
}
+
+syncTheme();
Таким образом до момента начала рендринга <html>
уже будет верно сконфигурирован под необходимую тему:
Переключить тему
Изображения
Переключение изображений можно организовать несколькими способами. Самый очевидный – это исключительно с помощью css и селекторов по атрибуту.
Объявляется пара селекторов: один для изображений светлой темы, а второй для изображений тёмной темы. Каждому из них так же добавляется одноимённая пара с атрибутом темы. Замысел заключается в переключении видимости изображения в зависимости от темы для которой предназначено изображение и текущего выбора пользователя:
/* По умолчанию изображения для светлой
темы - отображаются */
}
/* При выборе пользователем тёмной темы,
изображения для светлой - НЕ отображаются */
}
/* По умолчанию изображения для тёмной
темы - НЕ отображаются */
}
/* При выборе пользователем светлой темы,
изображения для тёмной - НЕ отображаются */
}
При следующем объявлении будет отображаться только одно изображение:
Недостатком данного способа является загрузка браузером обоих изображений, даже если не происходит переключение темы и потому одно из них совершенно не требуется. Так же, если возможна ситуация когда браузер не сможет подгрузить стили, либо они отключены у пользователя, то одновременно будут отображаться оба изображения.
Другой способ заключается в использовании JavaScript. Необходимо написать скрипт, который будет при переключении темы обновлять все изображения. Конкретных способов каким образом это можно сделать крайне много, всё зависит исключительно от фантазии, здесь же рассмотрим пример с <picture>
и <source>
.
Пусть имеется следующее объявление изображения:
В отличие от примера выше, здесь в <source>
отдельно перечислены оба источника, и для светлой, и для тёмной темы. Подобное определение необходимо в первую очередь для скрипта, а не браузера. Задача скрипта заключается в итерации по всем <pictures>
и принудительном указании какое именно изображение необходимо отображать. Достигается это простым путем, в самом верху иерархии динамически можно добавить новый <source>
без media
атрибута, таким образом подобный источник всегда будет являться приоритетным, именно его srcset
и будет использован браузером.
Здесь и далее предполагается, что код добавляется в пример из прошлого раздела. В данном случае дописываем syncTheme
:
Как результат работы данного кода все <picture>
получат новый источник с атрибутом srcset
указывающим на изображение текущей темы:
Поскольку syncTheme
для предотвращения морганий вызывается в самом начале, когда ещё невозможно обработать изображения, то при несоответствии текущей темы в системе и выбора пользователя, все изображения так же будут в рассинхронизированом состоянии. Поэтому код для обновления изображений необходимо вызвать по окончании загрузки всего документа.
Для этого в самый конец скрипта необходимо повесить обработчик события onload
которое ещё раз вызовет syncTheme
:
const setTheme = (isLight) => {
window.localStorage.setItem(STORAGE_KEY, THEME_NAME[+isLight]);
syncTheme();
}
syncTheme();
+
+window.onload = () => {
+ syncTheme();
+}
Отслеживание изменений в системе
У многих пользователей в ОС включено автоматические переключение тем в соответствии со временем суток. Хорошим решением будет отслеживание момента такого переключения и автоматической смены темы для соответствия ожиданиям.
window
PCS_DARK
'change',!isDark
Данный код можно добавить в самый конец скрипта.
Заголовок окна браузера
Браузер Safari и ряд мобильных браузеров поддерживают возможность задания цвета заголовка окна. Достигается это с помощью тега <meta>
и с атрибутом name
равным theme-color
. Значение цвета задается в атрибуте content
.
Необходимо задать цвета для заголовка с помощью переменных:
}
}
В секцию <head>
добавить:
И следующий код в тело syncTheme
:
;
;
if themeMeta
Стоит иметь в виду, что могут присутствовать различные ограничения на свободу выбора цвета. Как пример, в настольной версии браузера Safari невозможно задать ряд красных оттенков когда в системе выбрана светлая тема, это сделано для предотвращения визуального слияния с кнопкой закрытия окна. А при действии тёмной темы, в целом невозможно задать слишком светлые цвета, они автоматически будут заменены на серый.
Дополнительно
Чёрно-белые изображения
В отдельном ряду стоят чёрно-белые изображения, не обязательно создавать для них отдельную инвертированную версию, можно воспользоваться фильтрами:
}
}
Изображение ниже в оригинале имеет чёрный фон, поэтому для светлой темы оно полностью инвертируется invert(1)
, а для тёмной темы лишь незначительно осветляется для соответствия серому фону.

наведите курсор мыши
SVG-изображения
SVG изображения позволяют использовать CSS-переменные, но для этого необходимо либо чтобы файл со стилями был напрямую встроен внутри SVG изображения, либо само изображение было непосредственно встроено на страницу как в следующем примере. Так же не стоит забывать о возможности использования ключевого слова currentColor
, которое позволяет использовать текущее значение из свойства color
.
}
}
Элементы управления
Если сайт использует стандартные элементы управления, то обязательно следует указать цветовую схему и для них. Для этого используется специальное свойство color-scheme
которое может принимать значения light
или dark
:
Кнопочка
Тёмная тема:
Кнопочка
Светлая тема:
Если задать значение для
color-scheme
вlight dark
, появляется возможность использовать экспериментальную функциюlight-dark()
, она принимает два аргумента из значений цветов для светлой и тёмной тем, а возвращает соответствующее текущим настройкам системы/браузера.Пример:
color: light-dark(black, white);
На момент написания заметки функциональность присутствовала только в браузере Firefox. Текущий статус поддержки браузерами можно посмотреть здесь.
Плавное переключение цветов
При определённых условиях мгновенное переключение темы с тёмной на светлую может быть крайне неприятно для глаз пользователя. Хорошим решением будет добавить плавную анимацию перехода. Однако не стоит забывать про устройства которые не поддерживают быстрое обновление экрана (update: slow
), либо у пользователя включена опция снижения анимаций (prefers-reduced-motion
):
{
}
}
Итог
Рассмотренный код и большую часть приёмов можно скачать здесь:
Файл | MD5 |
---|---|
theme-toggle.zip | 9573441af33b9f1711f6b741adb4f5cf |
На этом всё.