Вы, наверное, уже знаете о том, что для хранения сведений об отдельных компонентах цвета можно применять пользовательские CSS-переменные. Это позволяет избавиться от необходимости повторения одних и тех же цветовых координат в стилях, описывающих цветовую тему сайта. Возможно, вы даже знаете о том, что одну и ту же переменную можно использовать для настройки нескольких компонентов цвета.
Например — тона (hue) и насыщенности (saturation) при использовании цветовой модели HSL:
:root {
--primary-hs: 250 30%;
}
h1 {
color: hsl(var(--primary-hs) 30%);
}
article {
background: hsl(var(--primary-hs) 90%);
}
article h2 {
background: hsl(var(--primary-hs) 40%);
color: white;
}
Вот — очень простая страница, спроектированная с использованием этого подхода.
Пример страницы
Пользовательские переменные, в отличие от переменных препроцессора, можно даже локально переопределять, используя их, например, для задания особого акцентного цвета для некоторых блоков:
:root {
--primary-hs: 250 30%;
--secondary-hs: 190 40%;
}
article {
background: hsl(var(--primary-hs) 90%);
}
article.alt {
--primary-hs: var(--secondary-hs);
}
Пример страницы, акцентный цвет одного из блоков которой переопределён
Всё это хорошо, всё это красиво, но лишь до тех пор, пока в игру не вступит тёмная тема. Идея использования пользовательских переменных для упрощения настройки тёмной темы не нова. Но в каждой из встретившихся мне статей, посвящённых этой задаче, предлагается создавать массу пользовательских свойств, по одному для каждого цвета, и переопределять свойства в медиа-запросе.
Это — нормальный подход, и вы, вероятно, тоже будете им пользоваться, по крайней мере — для настройки некоторой части ваших цветов. Правда, даже у самых дисциплинированных дизайнеров не все цвета представлены в виде CSS-переменных. Разработчики сайтов нередко пользуются встроенными определениями цветов, особенно это касается оттенков серого (в нашем примере такой подход использован для настройки цвета «подвала»). Это означает, что оснащение сайта тёмной темой превращается в довольно-таки трудоёмкое дело, которое, вполне возможно, отложат на потом, особенно — если речь идёт о неких второстепенных проектах.
Тот хитрый приём, который я хочу вам показать, может показаться грубоватым любому, кто достаточно много знает о цветах (прости, Крис!), но он, совершенно точно, поможет вам создать рабочую тёмную тему за считанные минуты. Результат нельзя будет назвать идеальным, и то, что получится, в итоге, для создания качественной тёмной темы, надо будет подстроить (и «тёмная тема», кстати, это не только результат замены одних цветов другими), но это — лучше чем ничего. То, что получится, может играть роль базы для дальнейшей работы.
Основная идея моего подхода заключается в том, чтобы использовать пользовательские CSS-свойства для хранения сведений о светлоте (lightness) цветов, а не полной информации о цветах. В тёмном режиме эти переменные переопределяются, в них записываются значения, вычисляемые по формуле 100% — lightness
. Это, в целом, позволяет получить светлые цвета из тёмных цветов, цвета средней светлоты из цветов средней светлоты, и тёмные цвета из светлых цветов. При таком подходе вполне можно пользоваться встроенными определениями цветов, необязательно применять переменные при настройке абсолютно всех цветов проекта. Вот как, учитывая вышесказанное, могут выглядеть стили демонстрационного проекта:
root {
--primary-hs: 250 30%;
--secondary-hs: 190 40%;
--l-0: 0%;
--l-30: 30%;
--l-40: 40%;
--l-50: 50%;
--l-90: 90%;
--l-100: 100%;
}
@media (prefers-color-scheme: dark) {
:root {
--l-0: 100%;
--l-30: 70%;
--l-40: 60%;
--l-90: 10%;
--l-100: 0%;
}
}
body {
background: hsl(0 0% var(--l-100));
color: hsl(0 0% var(--l-0));
}
h1 {
color: hsl(var(--primary-hs) var(--l-30));
}
article {
background: hsl(var(--primary-hs) var(--l-90));
}
article h2 {
background: hsl(var(--primary-hs) 40%);
color: white;
}
footer {
color: hsl(0 0% var(--l-40));
}
Ниже показан вид страницы этого проекта в светлом и тёмном режимах.
Светлая тема
Тёмная тема
Учитывайте то, что цвета для тёмной темы получены автоматически.
Обратите внимание на то, что мы заменили абсолютно все, без разбора, значения светлоты соответствующими переменными. Но, на самом деле, нет нужды поступать именно так. Например, заголовки статей будут смотреться приятнее и отличаться лучшей контрастностью в том случае, если мы попросту не будем менять их цвет.
Тёмная тема, при применении которой цвет заголовков статей не изменился
При создании этого варианта нашей тёмной темы автоматическая настройка цветов использована не для всех без исключения элементов. А именно, тут оставлен цвет фона и цвет текста для article > h2
.
Подобные решения, касающиеся выбора цветов, легко принимать, просматривая CSS-код, заменяя процентные значения светлоты цветов переменными и оценивая новый внешний вид страницы.
Проблема с цветовой моделью HSL
Почему заголовки статей легче читать в том случае, когда они, в тёмной теме, выводятся тем же цветом, что и в светлой, а не цветом, светлота которого инвертирована? Главная причина этого кроется в том, что компонент HSL-цвета, отвечающий за светлоту, на самом деле, не имеет отношения к той «светлоте», которую воспринимает человек. В результате одинаковые изменения светлоты могут приводить к очень разным изменениям яркости цветов, воспринимаемой человеком.
Именно это и можно назвать большой проблемой данного подхода: тут предполагается, что HSL-светлота имеет некий реальный смысл, но, и мы уже об этом говорили, на самом деле, это не так. Жёлтый и синий цвета, имеющие одну и ту же HSL-светлоту (50%), выглядят очень по-разному. Кроме того, анализируя HSL-цвета, можно обратить внимание на то, что тёмные цвета имеют меньшие отличия друг от друга, чем светлые цвета, так как цветовая модель HSL не является однородной для восприятия (в отличие от LCH и Lab).
Значит ли это, что предложенная мной методика работы с цветами не подходит ни для чего кроме временных тёмных тем, используемых во время разработки настоящих тёмных тем?
На самом деле — всё не так уж и мрачно.
Довольно скоро мы сможем пользоваться в браузерах LCH-цветами. Первая браузерная реализация поддержки таких цветов недавно появилась в Safari, производители других браузеров тоже работают в этом направлении.
Цветовое пространство LCH подходит для реализации моей методики гораздо лучше HSL, так как LCH-светлота — далеко не бессмысленный показатель, это — не просто разные варианты одного и того же цвета, различающиеся по яркости. LCH-светлота имеет отношение и к тону (hue) цвета, и к его цветности (chroma).
Для того чтобы «вживую» посмотреть этот пример — вам понадобится Safari TP 120+. Сравните два показанных там градиента. Верхний — это разные HSL-цвета со светлотой 50%, а нижний — это LCH цвета с той же светлотой. В верхней части окна есть слайдер, который позволяет посмотреть градиенты для другой светлоты. Если у вас нет Safari TP 120+ — ниже приведён скриншот примера.
HSL- и LCH-цвета с одной и той же светлотой
Обратите внимание на то, что некоторые HSL-цвета (вроде жёлтого и светло-голубого) гораздо светлее других. А вот все LCH-цвета одной светлоты имеют, собственно, одинаковую светлоту.
Тут стоит учитывать то, что компонент Chroma (цветность) модели LCH, на самом деле, не связан напрямую с компонентом Lightness (светлота) модели HSL. Поэтому, даже несмотря на то, что мы устанавливаем их в одинаковые значения, это не приводит к одним и тем же результатам.
Как адаптировать предложенную мной методику создания тёмных тем к работе с LCH-цветами?
Я использовала этот инструмент для преобразования существующих HSL-цветов в LCH-цвета, затем вручную немного подстроила получившиеся значения, так как цвета, после конверсии, не выглядели достаточно хорошо во всех вариантах LCH-светлоты. Обратите внимание на то, что HSL-цвета с одними и теми же тоном и насыщенностью могут иметь различные тон и цветность при их преобразовании к LCH-цветам. Обратное попросту сделало бы использование LCH-цветов бессмысленным!
Вот как результаты применения этого приёма выглядят на практике (для просмотра этого примера нужен браузер Safari TP 120 или более новый). Здесь тёмный режим сгенерирован автоматически путём инверсии значения переменной, отвечающей за светлоту LCH-цветов.
«Светлый» вариант сайта, созданного с применением LCH-цветов
«Тёмный» вариант сайта, созданного с применением LCH-цветов
Теперь не только тёмная тема сайта выглядит лучше. Даже его светлый вариант смотрится приятнее. В частности, два цвета, применяемые при стилизации похожих элементов, выглядят более близкими друг к другу из-за того, что имеют одну и ту же LCH-светлоту.
Слева — тёмная тема, основанная на модификации HSL-цветов, справа — тёмная тема, при создании которой использованы LCH-цвета
А ниже приведено анимированное сравнение этих двух тем.
lea.verou.me/wp-content/uploads/2021/03/hsl-dm.png
Анимированное сравнение тёмных тем
Обратите внимание на то, что в реальных проектах, до тех пор, пока LCH-цвета не будут как следует поддерживаться во всех браузерах, при использовании этих цветов нужно предусматривать запасной план действий с применением @supports
. Но тут, для краткости, я не включила в пример соответствующий код.
Автоматизация генерирования переменных, хранящих сведения о светлоте цветов
Если вы пользуетесь препроцессором, который поддерживает циклы, например — Sass, это значит, что вы можете автоматизировать генерирование переменных, хранящих сведения о светлоте цветов. При таком подходе легко создать набор переменных, хранящих значения светлоты, изменяющиеся с небольшим шагом, например — 5%:
:root {
@for $i from 0 through 20 {
--l-#{$i * 5}: #{$i * 5}%;
}
}
@media (prefers-color-scheme: dark) {
:root {
@for $i from 0 through 20 {
--l-#{$i * 5}: #{100 - $i * 5}%;
}
}
}
Можно ли сделать работу с переменными, хранящими сведения о светлоте, лучше соответствующей принципу DRY?
Кому-то из вас может не понравиться повторение значений. Например, нам нужно объявить переменную --l-40
и, для использования в светлом режиме, записать в неё 40%, а потом, при переходе в тёмный режим, в неё нужно записать 60%. Можно ли как-то получить это значение, просто вычитая уже имеющееся у нас значение из 100%?
Тот, у кого есть опыт в программировании, может попробовать нечто вроде этого:
--l-40: calc(100% - var(--l-40));
Но работать эта конструкция не будет. CSS — это не императивный язык программирования. Тут нет неких этапов вычислений, не выполняются какие-то шаги, когда переменные имеют разные значения до и после выполнения такого шага. Здесь нет концепции времени — все имеющиеся объявления вступают в силу одновременно. Работа CSS больше похожа на реактивное вычисление значений ячеек электронной таблицы с формулами, чем на вычисления, которые выполняют в JavaScript и в других языках программирования (есть универсальные реактивные языки программирования, но они не особенно широко известны). В результате объявления, похожие на то, которое показано выше, считаются циклическими: переменная --l-40
не может ссылаться сама на себя — поэтому возникает ошибка. При исправлении этой ошибки в дело вступает механизм, который устанавливает переменную --l-40
в её исходное значение (это происходит из-за того, что CSS не выбрасывает ошибок).
Итак, есть ли способ избежать двойного объявления переменных для хранения сведений о светлоте цвета, то есть — одной переменной для светлой темы, а второй — для тёмной?
Такой способ есть, но я не рекомендовала бы пользоваться им. Он ухудшает читабельность и понятность кода, не давая при этом особых преимуществ. Но, ради того, чтобы немного поупражняться в интеллектуальных задачах, я опишут тут этот метод.
Вместо того, чтобы записывать в --l-40
значение 40%, мы запишем в эту переменную значение, выражающее отличие необходимого нам значения от 50%. То есть — -10%
. В результате конструкция calc(50% + var(--l-40))
даст нам 40%, а конструкция calc(50% — var(--l-40))
даст 60%. Это — именно те значения, которые нам и нужны. А значит — мы можем объявить единственную переменную, которая равняется -1
в тёмном режиме и 1
в светлом. Потом мы просто будем умножать соответствующие значения на эту переменную.
Вот фрагмент кода, созданного с учётом вышеприведённых рассуждений:
:root {
--dm: 1;
/* Пример объявления переменной: */
--l-40: -10%;
}
@media (prefers-color-scheme: dark) {
:root {
--dm: -1;
}
}
/* Пример использования имеющихся переменных: */
footer {
color: hsl(0 0% calc(50% + var(--dm) * var(--l-40));
/* Некрасиво! */
}
Надеюсь, теперь вам понятна причина, по которой я не рекомендовала бы пользоваться этим приёмом. Он сильно усложняет работу, а взамен мы получаем небольшое приближение к идеалам DRY за счёт избавления от нескольких переменных. Это — пример навязчивого стремления к DRY, которое программисты, в конце концов, признают вредящим продуктивности их работы.
Как вы создаёте светлые и тёмные темы для своих проектов?
Автор: ru_vds