Перевод второй части статьи «A pragmatic guide to modern CSS colours - part two».

Автор: Kevin Powell, 2 декабря 2025

Читайте также: Практическое руководство по современным CSS-цветам — часть 1.

В конце статьи собраны ссылки на документацию MDN по CSS-функциям, используемым в статье.

Введение

В предыдущей статье автор рассмотрел практическое применение новых возможностей работы с цветом в CSS — с прицелом на разработчиков, которые чаще всего просто переносят значения из дизайн-макета в код.

С учётом того, сколько инструментов для работы с цветами появилось в CSS, браузер сегодня позволяет выполнять с цветами больше операций, чем многие графические редакторы. Это открывает широкий спектр новых сценариев и подходов к работе с цветом прямо в коде.

Термин Lightness в русском языке

Это примечание редактора, данного раздела нет в оригинальной статье.

Дословный перевод Lightness — это Светлота.

В повседневной речи ра��личие между «яркостью» и «светлотой» обычно не учитывается, и эти понятия часто используют как почти синонимичные. Однако в более точном употреблении между ними есть принципиальная разница.

Яркость применяют для описания физического свойства поверхности — степени освещённости и количества отражаемого света. Так, лист бумаги или снег на ярком солнце называют яркими, тогда как стены комнаты — светлыми.

Светлота — это субъективное ощущение яркости, зависящее от условий восприятия и особенностей наблюдателя. Это понятие относится скорее к области психологии: одна и та же объективная яркость может восприниматься по‑разному, и наоборот — одинаковая светлота может соответствовать разным уровням физической яркости.

В дизайне/цветовых моделях:

  • В модели HSL (Hue, Saturation, Lightness) lightness — именно светлота как параметр, описывающий субъективно интерпретируемую «светлость» цвета (0 = чёрный, 50% = нормальный цвет, 100% = белый).

  • В модели HSV (Hue, Saturation, Value) value обычно называют яркостью — это максимальная интенсивность каналов RGB.


Работа с цветами

В предыдущей статье мы разобрали несколько базовых сценариев использования относительных цветов. Кратко напомним: синтаксис выглядит примерно так:

:root {
  --primary: #ff0000;
}

.primary-bg-50-opacity: {
  background: hsl(from var(--primary) h s l / .5);
}

Ключевой момент в примере выше — буквы h s l являются не просто символами, а переменными, которые содержат значения оттенка (hue), насыщенности (saturation) и светлоты / яркости (lightness) исходного цвета.

Эти переменные можно заменять конкретными значениями. Например, в цвете #00ff00 отсутствует синяя составляющая, поэтому её можно добавить, подставив числовое значение вместо b (который в данном случае равен 0).

.green-with-a-touch-of-blue {
  color: rgb(from #00ff00 r g 25);
}

Это работает, но только если вы знаете, сколько синего было в исходном цвете. Жёстко заданное значение 25 действительно может увеличить долю синего, но, например, для цвета #00ff55 оно, наоборот, уменьшит это значение.

Настоящая сила появляется при использовании calc():

.green-with-a-touch-of-blue {
  color: rgb(from #00ff00 r g calc(b + 25));
}

Хотя выше используется rgb(), Kevin Powell говорит о том, что ему не удобно с ним работать. На практике удобнее использоватьhsl() и oklch() — так как они несут больше физического смысла.

В hsl() и oklch() значение hue (тон) идёт по кругу от 0 до 360. Если превысить 360 — значение «зацикливается». Поэтому если прибавить 180 к тону, получится цвет с противоположной стороны цветового круга.

Пример, где от базового цвета строятся вторичный и третичный через смещение тона:

:root {
	--color-primary: #2563eb;
  
	--color-secondary: hsl(from var(--color-primary) calc(h + 120) s l);
	--color-tertiary: hsl(from var(--color-primary) calc(h - 120) s l);
}

https://codepen.io/kevinpowell/pen/KwVLwdo


Упрощённое получение светлее/темне��

Подход со смещением каналов делает простым создание более светлых/тёмных вариантов:

:root {
	--primary-base: hsl(221 83% 50%);

	--primary-100: hsl(from var(--primary-base) h s 10%);
	--primary-200: hsl(from var(--primary-base) h s 20%);
    --primary-300: hsl(from var(--primary-base) h s 30%);
}

Но часто хочется не «жёстко поставить светлоту на 10%/20%», а просто сделать цвет немного светлее/темнее относительно текущей светлоты базового цвета.

Здесь снова помогает calc():

:root {
  --color-primary-base: #2563eb;
  --color-primary-lighter: hsl(from var(--color-primary-base) h s calc(l + 25));
  --color-primary-darker: hsl(from var(--color-primary-base) h s calc(l - 25));
}

https://codepen.io/kevinpowell/pen/vELwYoO


Иерархия поверхностей (surface levels)

Один из практичных сценариев — построение уровней поверхностей. В светлых темах для визуального разделения слоёв обычно достаточно теней. Однако на тёмных фонах тени дают слабый эффект и почти не помогают различать уровни.

В тёмной теме более надёжный подход — делать каждый следующий уровень поверхности немного светлее предыдущего. Реализовать это можно с помощью light-dark() и кастомных CSS-переменных:

/* тени показаны в примере CodePen ниже */

:root {
	--surface-base-light: hsl(240 67% 97%);
	--surface-base-dark: hsl(252 21% 9%);
}

.surface-1 {
	background: light-dark(
        var(--surface-base-light),
        var(--surface-base-dark)
    );
}

.surface-2 {
	background: light-dark(
        var(--surface-base-light),
        hsl(from var(--surface-base-dark) h s calc(l + 4))
    );
}

.surface-3 {
	background: light-dark(
        var(--surface-base-light),
        hsl(from var(--surface-base-dark) h s calc(l + 8))
    );
}

https://codepen.io/kevinpowell/pen/VYeOKQj

Создание цветовой схемы

Изменение только яркости подходит для простых случаев, но при построении полноценной палитры чаще используют perceptual colour scaling — перцептивное масштабирование цвета. В этом подходе слегка меняются не только яркость, но также тон и насыщенность.

Как правило, при увеличении яркости:

  • насыщенность немного повышают;

  • тон смещают в сторону более «холодных» значений;

а при затемнении цвета:

  • насыщенность уменьшают.

Такие корректировки удобно выполнять небольшими сдвигами каналов через calc() при построении оттенков.

:root {
	--primary-base: hsl(221 83% 50%);

	--primary-400: hsl(from var(--primary-base) calc(h - 3) calc(s + 5) 60%);
    --primary-300: hsl(from var(--primary-base) calc(h - 6) calc(s + 10) 70%);
}

В таком подходе различия особенно заметны на светлых шагах палитры (например, 100/200/300): при изменении только яркости цвет выглядит менее насыщенным и визуально более плоским.

https://codepen.io/kevinpowell/pen/JoYaoGW/5407077415578757498bf930eb55f8d4

Это требует небольшой первоначальной настройки, но после этого выбранный подход стабильно работает для всей цветовой палитры.

https://codepen.io/kevinpowell/pen/ogxxyjB

Эту идею можно развить дальше, используя более продвинутую математику — как показал Matthias Ott на CSS Day 2024 (ссылка ведёт сразу на нужный фрагмент доклада).


Перцептивные цвета и проблема HSL

Одна из причин популярности hsl() — его предсказуемость. Однако у этого подхода есть недостаток: даже при одинаковых значениях насыщенности и яркости разные оттенки могут восприниматься как более светлые или более тёмные.

Например, зелёный и синий могут иметь одинаковые значения s и l, но при этом контраст текста с фоном будет существенно отличаться.

https://codepen.io/kevinpowell/pen/myegNOY

Для решения этой проблемы используется oklch().

Правильная яркость с oklch()

Функция oklch() концептуально похожа на hsl(), но основана на цветовой модели LCH (её также называют HCL). Эта модель разработана так, чтобы воспринимаемая яркость оставалась более равномерной при переходе между разными оттенками.

В модели LCH используются три компонента: lightness, chroma, hue.

Первое значение — яркость (lightness) в диапазоне от 0 до 1
(при желании можно задавать в процентах);

Третье значение — тон (hue), задаваемый углом, как и в hsl(),
но с важным отличием: в hsl() значение соответствует красному,
тогда как в LCH — это пурпурный;

В этом примере на CodePen оба образца цвета отображаются под одним и тем же углом, и, как вы можете видеть, они довольно сильно отличаются:

https://codepen.io/kevinpowell/pen/NPxVPwY

Второе значениеchroma (хрома), близкое по смыслу к насыщенности: 0 даёт нейтральный серый цвет, а чем больше значение, тем «чище» цвет.

Шкала хромы начинается с 0 и… на этом всё становится немного странным.

Теоретически у значения хромы нет верхнего предела — цветовые модели в этом смысле устроены довольно странно. На практике же максимальные значения обычно находятся в районе 0.4; именно этому примерно соответствует 100%, если задавать хрому в процентах, а не безразмерным числом.

Использование процентов выглядит самым разумным вариантом, однако у хромы есть серьёзная проблема: её допустимый максимум меняется в зависимости от тона и яркости. Из-за этого можно получить весьма неожиданные результаты.

https://codepen.io/kevinpowell/pen/KwVLwoG

Kevin Powell пищет, что с большим интересом ждал появления LCH в CSS, однако на практике он часто продолжает использовать hsl() — из-за переменного верхнего предела хромы и странных сдвигов цвета, которые иногда возникают, как видно на примерах выше.

Несмотря на все неудобства, связанные с хромой, у этого подхода остаются важные преимущества. Более широкий цветовой охват — это приятно, но главное — уверенность в том, что воспринимаемая яркость остаётся одинаковой при смене оттенков, что во многих случаях оказывается действительно полезным.

https://codepen.io/kevinpowell/pen/QwyRKPZ


Самая сложная часть — подобрать начальный OKLCH-цвет. Например можно использовать color picker с визуализацией ограничений хромы при изменении яркости и тона.

Но есть и другой путь благодаря relative colors! Можно использовать oklch() поверх базовых цветов, которые всё ещё задаются через hsl().

Пример для плашек уведомлений:

.toast {
  --base-color: hsl(225, 87%, 56%);
}

[data-toast="info"] {
  --toast-color: oklch(from var(--base-toast-color) l c 275);
}

[data-toast="warning"] {
  --toast-color: oklch(from var(--base-toast-color) l c 80);
}

[data-toast="error"] {
  --toast-color: oklch(from var(--base-toast-color) l c 35);
}

В версии с oklch() стили выглядят более согласованными: заметнее всего это в границах (border), где контраст между фоном и границей в hsl()-вариантах может сильно «плавать». Также ощущается более ровная «перцептивная насыщенность» между вариантами.

https://codepen.io/kevinpowell/pen/wBMbozY


oklch() vs lch()

Стоит отметить, что в CSS доступны как oklch(), так и lch() (а также oklab() и lab()). Цветовое пространство LCH изн��чально создавалось с целью как можно точнее соответствовать человеческому восприятию цвета при переходе между оттенками.

LCH появилось ещё в 1976 году и имело ряд недостатков, особенно заметных в диапазонах синего и фиолетового. Поэтому в 2020 году была разработана модель OKLCH — обновлённая версия LCH, устраняющая эти проблемы.

Если требуется более подробное сравнение, можно обратиться к отдельной статье, однако на практике всё можно упростить и просто использовать oklch().


Смешивание двух цветов: color-mix()

Относительные цвета полезны, когда нужно модифицировать каналы одного цвета. Но иногда нужно смешать два разных цвета. Для этого есть color-mix():

.purple {
  color: color-mix(in srgb, red, blue);
}

https://codepen.io/kevinpowell/pen/jEbvOpX/514300329dd9a7b7c7412d9ab349a528

Пока нужно указывать цветовое пространство

Возможно, вы заметили in srgb в качестве первого аргумента в примере выше. На данный момент в color-mix() нужно явно указывать, в каком цветовом пространстве выполнять смешивание — и разные пространства могут давать заметно разные результаты.

https://codepen.io/kevinpowell/pen/MYKdwVK

Kevin Powell делится опытом и рассказывает, что сначала пробует oklab, затем oklch, и в большинстве случаев один из этих вариантов его устраивает. Иногда он дополнительно экспериментирует с другими цветовыми пространствами, чтобы посмотреть, какой результат они дают.

Кроме того, рабочая группа CSS недавно приняла решение сделать oklab значением по умолчанию. Когда браузеры реализуют это изменение, указывать цветовое пространство станет необязательно — хотя при необходимости такая возможность останется.


Управление долей каждого цвета

При использовании color-mix() по умолчанию берётся по 50% каждого цвета. При этом можно явно указать, какую долю каждого цвета использовать.

.red-with-a-touch-of-blue {
  background: color-mix(in oklab, red 90%, blue);
}

.or-like-this {
  background: color-mix(in oklab, red, blue 10%);
}

https://codepen.io/kevinpowell/pen/xxQMvKR


Прозрачность в color-mix()

Существует два способа получить прозрачный результат. Первый — когда суммарное значение долей цветов меньше 100%.

.semi-opaque {
  background: color-mix(in oklab, red 60%, blue 20%);
}

Итоговая сумма процентов напрямую определяет значение альфа-канала. В приведённом выше примере альфа будет равна 80%. Если же сумма превышает 100%, значения автоматически нормализуются до 100%.

https://codepen.io/kevinpowell/pen/gbPJamY

Также можно смешивать цвет с transparent.

.thiry-percent-opacity-red {
  background: color-mix(in oklch, red 30%, transparent);
}

Это работает, но если цель именно в управлении прозрачностью, Kevin Powell советует использовать относительные цвета.

Зато color-mix() удобно применять для создания «полосатых» градиентных эффектов без необходимости вручную рассчитывать все промежуточные значения. Это довольно нишевый сценарий, но его преимущество — в простоте реализации.

https://codepen.io/kevinpowell/pen/VYeOvYE


В будущем станет проще: кастомные функции CSS

Одна из проблем многих новых возможностей CSS — большое количество повторяющегося кода при их использовании. К счастью, в CSS появляются кастомные функции, которые должны заметно упростить работу с такими сценариями.

@function --lower-opacity(--color, --opacity) {
  result: oklch(from var(--color) l c h / var(--opacity);
}

.lower-opacity-primary {
  background: --lower-opacity(var(--primary), .5);
}
@function --shade-100(--color) returns <color> {
  result: hsl(from var(--color) calc(h - 12) calc(s + 15) 95%);
}
@function --shade-200(--color) returns <color> {
  result: hsl(from var(--color) calc(h - 10) calc(s + 12) 85%);
}

.call-to-action {
  background: --shade-200(var(--accent));
}

.hero {
  background: --shade-800(var(--primary));
  color: --shade-100(var(--primary));
}

Всё сильно изменилось

Хотя многие разработчики по-прежнему просто копируют значения из дизайн-файлов, возможности CSS уже позволяют делать больше, чем доступно в большинстве дизайн-приложений: смешивание цветов, относительные цвета, расширенные цветовые гаммы и многое другое.

Часть рассмотренных подходов действительно требует предварительной настройки, но после этого можно выстраивать очень устойчивые и гибкие системы. На фоне статичности большинства дизайн-инструментов это закономерно поднимает вопрос о том, стоит ли переносить большую часть дизайнерской работы непосредственно в браузер.


Ссылки на MDN по теме статьи

Примечание редактора: этого раздела нет в оригинальной статье.

Цветовые функции

Работа с цветами

Переменные и вычисления

Будущие возможности

Комментарии (0)