Когда дизайнер просит «осветлить кнопку на 10 %», в hex- или RGB-нотации это превращается в три отдельных подбора: красный канал, зелёный, синий, и проверка, не получился ли в итоге грязно-розовый. Так устроена сама модель: байты R, G, B описывают, сколько каждого луча подмешать в пиксель, но человеческий глаз воспринимает цвет иначе — через яркость, насыщенность и тон. oklch() — это запись цвета именно в этих трёх координатах, поэтому многие задачи UI (тёмная тема, ховер-состояния, согласованные палитры) с ней решаются в одну строку вместо ручного подбора.
Ниже — разбор формата по компонентам, его сравнение с близкими функциями (hsl(), oklab(), lch()), относительный синтаксис, нюансы P3-гаммы и реальный pitfall с градиентами. Про похожую по идее, но менее точную модель уже была отдельная статья про HSL — OKLCH её, по сути, заменяет в большинстве задач.
Что внутри: три параметра и один опциональный
Базовый синтаксис в его absolute-форме выглядит так:
color: oklch(L C H); /* непрозрачный */
color: oklch(L C H / A); /* с альфой */
Каждая буква — независимая шкала:
- L — насколько светлый цвет (в спецификации называется lightness). Число 0…1 или 0%…100%. 0 — чёрный, 1 — белый. Главное отличие от HSL: при одинаковом L два разных цвета будут одинаково яркими на восприятие, а не на бумаге.
- C — насколько насыщенный цвет (в спецификации — chroma). Диапазон 0…~0.4. 0 — чистый серый (тон в этот момент игнорируется), 0.4 — максимум насыщенности для текущего тона.
- H — тон (hue), угол на цветовом круге 0…360 (или 0deg…360deg). ~30 — красно-оранжевый, ~140 — зелёный, ~250 — синий.
- A — прозрачность (alpha), 0…1 или процент. Опциональна, по умолчанию 1.
Серия минимальных примеров, в каждом меняется только один параметр — остальные одинаковые:
/* светлый, средний и тёмный синий — одинаковые C и H */
.bg-50 { background: oklch(0.95 0.10 250); }
.bg-500 { background: oklch(0.55 0.10 250); }
.bg-900 { background: oklch(0.20 0.10 250); }
/* блёклый, средний и насыщенный — одинаковые L и H */
.muted { background: oklch(0.55 0.04 30); }
.regular { background: oklch(0.55 0.15 30); }
.vivid { background: oklch(0.55 0.27 30); }
/* палитра одного «уровня» — меняется только тон */
.tag-red { background: oklch(0.65 0.18 25); }
.tag-yellow { background: oklch(0.65 0.18 95); }
.tag-green { background: oklch(0.65 0.18 145); }
.tag-blue { background: oklch(0.65 0.18 250); }
/* альфа — два эквивалентных способа */
.tint-a { background: oklch(0.60 0.20 250 / 0.5); }
.tint-b { background: oklch(0.60 0.20 250 / 50%); }
Pitfall: насыщенность в процентах считается не как обычно. 100% для C равняется 0.4, а не 1. Спецификация так зафиксировала верхнюю границу, потому что значения > 0.4 на практике почти не используются — они выходят за пределы любых физически существующих экранов. Из-за этого oklch(50% 100% 240) и oklch(50% 0.4 240) — один и тот же цвет.
Чем отличается от HSL, oklab и lch
Внешне формула oklch() похожа на hsl(): три параметра, среди них яркость и тон. Разница не в записи, а в том, что под капотом.
В HSL под L понимается арифметическая середина между минимальным и максимальным каналом — математика красивая, но к глазу отношения не имеет. Поэтому hsl(60 100% 50%) (жёлтый) на восприятие сильно ярче, чем hsl(240 100% 50%) (синий), хотя число у обоих одинаковое. OKLCH построен на цветовом пространстве Oklab, которое подбиралось эмпирически так, чтобы равные L давали действительно одинаково яркие цвета:
/* HSL: одинаковая L=50%, но жёлтый «жжёт», синий «спит» */
.h-yellow { background: hsl(60 100% 50%); }
.h-blue { background: hsl(240 100% 50%); }
/* OKLCH: одинаковая L=0.6 — оба цвета воспринимаются одинаково светлыми */
.o-yellow { background: oklch(0.6 0.2 95); }
.o-blue { background: oklch(0.6 0.2 250); }
Практический бонус: на палитре бейджей в первом варианте белый текст на синем будет нормально читаться, а на жёлтом — исчезнет. Во втором палитра ведёт себя ровно: или белый текст подходит ко всем сразу, или ни к одному.
Сравнение с двумя другими модерн-функциями:
- oklab(L a b) — то же цветовое пространство, но запись не в полярных координатах. a — смещение зелёный/красный, b — синий/жёлтый. Удобно, когда нужно сдвинуть цвет «в сторону» на фиксированную величину, а не повернуть по кругу. Для палитр на сайте полярный вид понятнее.
- lch(L C H) — почти такой же синтаксис, но построен на старом пространстве CIELAB. У него та же идея перцептивной равномерности, но Oklab лучше справляется с насыщенными синими и фиолетовыми: в CIELAB они «уходят в фиолетовый» при изменении L.
И самый коварный нюанс. У всех трёх функций (hsl, lch, oklch) тон записывается углом, но нулевые градусы у них в разных местах круга:
hsl(0 100% 50%) /* красный */
lch(50% 60 0) /* красный */
oklch(0.6 0.2 0) /* розово-малиновый — НЕ красный */
oklch(0.6 0.2 30) /* вот теперь красный */
Это та ловушка, в которую попадают почти все, кто переписывает палитру из HSL подстановкой имён функций. Если статью читать одной фразой: в OKLCH красный — это примерно 30°, а не 0°.
Как пользуются на практике: палитра из одной яркости
Самая частая задача, ради которой переписывают цвет на OKLCH — согласованная палитра. Берётся одна яркость (L), одна насыщенность (C) и перебирается только тон (H):
:root {
--tag-l: 0.72;
--tag-c: 0.14;
}
.tag-css { background: oklch(var(--tag-l) var(--tag-c) 250); color: #fff; }
.tag-html { background: oklch(var(--tag-l) var(--tag-c) 30); color: #fff; }
.tag-js { background: oklch(var(--tag-l) var(--tag-c) 90); color: #fff; }
.tag-www { background: oklch(var(--tag-l) var(--tag-c) 150); color: #fff; }
.tag-jquery { background: oklch(var(--tag-l) var(--tag-c) 200); color: #fff; }
В HSL такой подход даёт пять разноярких заливок: на жёлтом текст почти не виден, на синем — контраст с запасом. В OKLCH у всех пяти заливок физически разная яркость в люменах, но воспринимаются они как одинаковые, и белый текст ведёт себя одинаково везде. Подробнее про подбор контрастного цвета шрифта — в отдельной статье.
Палитра градаций одного тона строится так же, только меняется яркость (L):
:root {
--brand-h: 250;
--brand-c: 0.18;
}
.brand-50 { background: oklch(0.96 0.04 var(--brand-h)); }
.brand-100 { background: oklch(0.90 0.06 var(--brand-h)); }
.brand-300 { background: oklch(0.75 0.12 var(--brand-h)); }
.brand-500 { background: oklch(0.55 var(--brand-c) var(--brand-h)); }
.brand-700 { background: oklch(0.40 0.16 var(--brand-h)); }
.brand-900 { background: oklch(0.22 0.10 var(--brand-h)); }
Насыщенность (C) в крайних значениях полезно сбавлять — почти-белые и почти-чёрные с её максимумом выглядят искусственно. Такая раскладка лежит в основе модерн-палитр (Radix, Open Props, Material You).
Тёмная тема в одну линию: оттенки не пересчитываются дизайнером заново, а зеркалятся по L. Подробнее про сам медиа-запрос — в материале про тёмную тему.
:root {
--surface: oklch(0.97 0.01 250);
--on-surface: oklch(0.22 0.04 250);
}
@media (prefers-color-scheme: dark) {
:root {
--surface: oklch(0.18 0.02 250); /* 0.97 → 0.18 */
--on-surface: oklch(0.92 0.04 250); /* 0.22 → 0.92 */
}
}
Относительный синтаксис: oklch(from …)
Самое полезное расширение OKLCH — относительная форма. Она берёт уже существующий цвет, раскладывает его на l, c, h и позволяет переписать какие-то из компонентов:
oklch(from <color> L C H [ / A])
Внутри функции значения каналов исходного цвета доступны как имена — l, c, h, alpha. Их можно использовать как есть или подставить в calc().
Осветлить любой бренд-цвет на фиксированный шаг:
:root { --brand: #1d4ed8; }
.btn { background: var(--brand); }
.btn:hover { background: oklch(from var(--brand) calc(l + 0.08) c h); }
.btn:active { background: oklch(from var(--brand) calc(l - 0.05) c h); }
Самое главное: --brand может быть в любом формате — hex, rgb, hsl, oklch — и логика осветления одинаково работает. Раньше для этого писали SCSS-функции на этапе сборки, теперь — одна строка в рантайме.
Снять насыщенность для disabled-состояния:
.btn:disabled {
background: oklch(from var(--brand) l calc(c * 0.25) h);
color: oklch(from var(--brand) calc(l + 0.2) calc(c * 0.25) h);
}
Привести разные цвета к одинаковой яркости — полезно, когда дизайнер дал палитру в hex и просит сделать так, чтобы белый текст читался везде:
:root {
--c1: #db2777; /* L ≈ 0.58 */
--c2: #16a34a; /* L ≈ 0.61 */
--c3: #ca8a04; /* L ≈ 0.67 */
}
.badge-1 { background: oklch(from var(--c1) 0.55 c h); color: #fff; }
.badge-2 { background: oklch(from var(--c2) 0.55 c h); color: #fff; }
.badge-3 { background: oklch(from var(--c3) 0.55 c h); color: #fff; }
Прозрачная вуаль из исходного цвета для оверлеев:
.overlay { background: oklch(from var(--brand) l c h / 0.15); }
Сдвинуть тон на пару градусов для пары акцентных кнопок:
.btn-primary { background: oklch(from var(--brand) l c h); }
.btn-secondary { background: oklch(from var(--brand) l c calc(h + 25)); }
Сюда же относится трюк с ключевым словом none: оно говорит «этот канал заранее не задан, при смешивании пусть подставится со стороны соседа». На уровне обычной CSS-функции это эквивалент 0, но в градиентах поведение отдельное (см. ниже).
Цвета шире, чем sRGB
OKLCH адресует не только привычное цветовое пространство sRGB, но и более широкое — Display P3. Это «живые» цвета, которых в sRGB физически нет: насыщенно-красные стоп-сигналы, флуоресцентно-зелёные, глубокие фиолетовые. На Apple-устройствах P3-экраны стоят с 2016 года, на современных Android-флагманах и MacBook — по умолчанию.
/* красный, который sRGB не может — на P3-экране заметно сочнее */
.poster-accent { background: oklch(0.62 0.28 25); }
/* для сравнения: максимально насыщенный sRGB-красный */
.poster-fallback { background: oklch(0.62 0.18 25); }
На sRGB-мониторе разницы между этими двумя строками почти не будет: браузер сам «утолкает» цвет в доступную гамму. На P3-экране первая строка засветится по-настоящему. Если хочется отдать честный фолбек, а на P3 показать что-то более насыщенное — для этого ровно и существует медиа-запрос @media (color-gamut: p3), разобранный в отдельной статье:
.poster {
background: oklch(0.62 0.18 25);
}
@media (color-gamut: p3) {
.poster {
background: oklch(0.62 0.28 25);
}
}
Градиенты: где спрятан подвох
Главный минус OKLCH — не сам формат, а его поведение в градиентах. Тон (H) — это угол, и между двумя углами всегда есть два пути: короткий и длинный. Между красным (~30deg) и синим (~250deg) короткий идёт через фиолетовый, длинный — через жёлтый и зелёный. Браузер должен выбрать.
В обычном linear-gradient(red, blue) цвет интерполируется в sRGB — и градиент проходит через серо-фиолетовую кашу посередине. С указанием цветового пространства результат становится перцептивно ровным, но появляется вопрос про путь по кругу:
/* старая школа: серое болото в центре */
.g-srgb {
background: linear-gradient(to right, oklch(0.6 0.2 30), oklch(0.6 0.2 250));
}
/* по короткому пути в OKLCH — через пурпурный */
.g-shorter {
background: linear-gradient(in oklch shorter hue, oklch(0.6 0.2 30), oklch(0.6 0.2 250));
}
/* по длинному пути — через жёлтый и зелёный, как у радуги */
.g-longer {
background: linear-gradient(in oklch longer hue, oklch(0.6 0.2 30), oklch(0.6 0.2 250));
}
По умолчанию выбирается shorter hue, и в большинстве случаев это правильно. Но если градиент идёт между близкими цветами, расположенными по разные стороны от нуля (например, тёмно-красный 355deg и красно-оранжевый 15deg), shorter hue пойдёт через нужный кратчайший путь в 20° — а вот longer hue в той же паре сделает 340° по кругу через зелёное и синее. Поэтому переключатель важен.
На многоступенчатых градиентах помогает фиксировать L и C равными и менять только H — это даёт идеальную «радугу» одной яркости:
.rainbow {
background: linear-gradient(
in oklch longer hue,
oklch(0.7 0.2 0),
oklch(0.7 0.2 360)
);
}
Фолбек для старых браузеров
Формально oklch() поддерживают все актуальные движки с 2023 года, относительный синтаксис — чуть позже. На старых движках цвет просто проигнорируется. Cтандартный паттерн — писать две строки подряд, первая работает везде, вторая перекрывает:
.btn {
background: #1d4ed8; /* sRGB-фолбек */
background: oklch(0.55 0.18 250); /* современный */
}
Если хочется надёжнее, есть детектор фичи через @supports:
.btn { background: #1d4ed8; }
@supports (color: oklch(0 0 0)) {
.btn { background: oklch(0.55 0.18 250); }
}
Для относительного синтаксиса детектор пишется отдельно — он добавился позже, и есть сборки браузеров с поддержкой oklch(), но без from:
@supports (color: oklch(from red l c h)) {
/* можно использовать относительный oklch */
}
Итог
RGB и hex останутся базовыми по понятным причинам — их понимает любая среда, от Figma до старой почтовой клиента. Но в местах, где надо вычислять цвета — светлеть, темнеть, унифицировать яркость, строить градиенты — oklch() убирает из работы всю невнятную «подгонку каналов». Достаточно один раз сложить мысленный словарь: L — это насколько светло, C — это насколько насыщенно, H — это в какую сторону спектра, — и большинство задач палитры начинают решаться арифметикой одного канала. P3-цвета и относительный синтаксис — бонус сверху, ради которого многие команды переходят на OKLCH вообще без оглядки на старые движки.
Комментарии (0)