CSS color-mix() и относительные цвета: оттенки, hover и прозрачность
color-mix() смешивает два цвета в выбранном цветовом пространстве, а относительный синтаксис меняет один канал готового цвета, например oklch(from var(--brand) calc(l - 0.1) c h). Оба считаются в браузере, работают с CSS-переменными и заменяют большую часть цветовых функций Sass. Для оттенков и hover смешивайте в oklab, для прозрачности в srgb.
Читать 6 мин
Как работает color-mix()
color-mix() принимает цветовое пространство и два цвета с необязательными долями и возвращает цвет между ними. color-mix(in oklab, var(--brand) 90%, black) значит 90% фирменного цвета и 10% чёрного, смешанные в Oklab. Если указана одна доля, второму цвету достаётся остаток. Если ни одной, каждому по 50%.
Результат браузер считает на лету, поэтому функция работает с CSS-переменными, currentColor и переключением темы. Функции Sass выполняются один раз при сборке и значений var() не видят.
Если доли в сумме меньше 100%, цвет ещё и становится полупрозрачным: color-mix(in srgb, red 30%, blue 30%) даёт смесь поровну с непрозрачностью 60%. Держите сумму ровно 100%, если вам это не нужно.
CSS
.button {
--brand: #2563eb;
background: var(--brand);
}
.button:hover {
background: color-mix(in oklab, var(--brand) 90%, black);
}
.badge {
background: color-mix(in oklab, var(--brand) 10%, white);
border-color: color-mix(in srgb, var(--brand) 30%, transparent);
}В каком пространстве смешивать: srgb, oklab или oklch
Пространство меняет результат сильнее, чем обычно ожидают. Ниже чистый синий #0000ff, смешанный поровну с чистым жёлтым #ffff00. Всё посчитано той же математикой, что использует браузер.
В srgb каналы усредняются ровно до 128, 128, 128, и получается #808080, серый с нулевой насыщенностью. Oklab тоже смешивает по прямой, а синий и жёлтый лежат по разные стороны от серой оси, поэтому почти гасят друг друга и дают блёклый серо-голубой. Oklch интерполирует угол тона: идёт коротким путём от 264° к 110°, и середина оказывается яркой бирюзой с тоном 187° и насыщенностью исходных цветов.
- Синий #0000ff#0000ff
- Жёлтый #ffff00#ffff00
- в srgb: #808080#808080
- в oklab: #6cabc7#6cabc7
- в oklch: #00bdb0#00bdb0
| Пространство | Синий + жёлтый | OKLCH L, C | Blue-600 + yellow-400 | OKLCH L, C |
|---|---|---|---|---|
| srgb | #808080 | 0.600, 0 | #909880 | 0.665, 0.035 |
| oklab | #6cabc7 | 0.710, 0.077 | #90a3ad | 0.703, 0.026 |
| oklch | #00bdb0 | 0.710, 0.262 | #00bd9e | 0.703, 0.194 |
Вторая пара это #2563eb и #facc15 из Tailwind. Результаты oklch выходят за охват sRGB, поэтому насыщенность указана до приведения к охвату, а hex это ближайший цвет, который способен показать sRGB-экран.
Бросаются в глаза две вещи. Srgb даёт светлоту 0.600, а перцептивная середина между синим (0.452) и жёлтым (0.968) равна 0.710: отсюда тёмная грязная середина у градиентов в srgb. И только oklch сохраняет смесь яркой, зато проходит через тон, которого нет ни у одного из исходных цветов.
Простое правило: оттенки, тени и hover смешивайте в oklab, там тон не уходит и шаги светлоты ровные. Oklch берите, когда оба цвета насыщенные и нужен насыщенный результат. Srgb оставьте для прозрачности, так браузер и накладывает слои.
Рецепты: hover, светлые фоны и прозрачные варианты
Во всех примерах --brand равен #2563eb. Белый текст на нём даёт контраст 5.17:1.
Десять процентов чёрного в oklab затемняют сильнее, чем в srgb: контраст белого текста растёт до 6.50:1 против 6.11:1. Когда второй цвет чёрный или белый, oklab и oklch дают одинаковый результат, потому что у них нет тона для интерполяции.
| Рецепт | CSS | Результат | Контраст |
|---|---|---|---|
| Hover, на 10% темнее | color-mix(in oklab, var(--brand) 90%, black) | #1f55cc | 6.50:1 с белым текстом |
| Active, на 20% темнее | color-mix(in oklab, var(--brand) 80%, black) | #1947ae | 8.22:1 с белым текстом |
| Hover в srgb | color-mix(in srgb, var(--brand) 90%, black) | #2159d3 | 6.11:1 с белым текстом |
| Светлый фон, 10% | color-mix(in oklab, var(--brand) 10%, white) | #e8f0ff | 4.51:1 с фирменным текстом |
| Светлый фон, 20% | color-mix(in oklab, var(--brand) 20%, white) | #d2e2ff | 3.95:1 с фирменным текстом |
| Прозрачный, 20% | color-mix(in srgb, var(--brand) 20%, transparent) | rgba(37, 99, 235, 0.2) | На белом выглядит как #d3e0fb |
Следите за светлыми фонами. Фирменный текст на белом даёт 5.17:1, на фоне с 10% цвета уже 4.51:1, а на фоне с 20% всего 3.95:1, и это не проходит WCAG AA для обычного текста. Если фирменный текст стоит на фирменной подложке, берите для текста цвет из hover.
Смешивание с transparent не трогает сам цвет и снижает только альфу, потому что браузер смешивает предумноженные значения. Такой цвет подстраивается под то, что под ним: одно и то же правило на белом выглядит как #d3e0fb, а на тёмной подложке #0f172a как #132651. Смесь с белым на тёмной странице так и останется бледным пятном, поэтому для компонентов из обеих тем прозрачный вариант обычно лучше.
Относительный синтаксис: меняем один канал
Относительный синтаксис берёт готовый цвет, раскладывает его на каналы и даёт переписать любой из них через calc(). В oklch(from var(--brand) l c h) ключевые слова l, c и h содержат светлоту (от 0 до 1), насыщенность и тон цвета --brand.
В отличие от color-mix(), он меняет ровно одно свойство: светлоту без насыщенности, тон без светлоты или прозрачность у цвета, который хранится в hex.
| Рецепт | CSS | Результат из #2563eb | Белый текст |
|---|---|---|---|
| Темнее | oklch(from var(--brand) calc(l - 0.1) c h) | #0442c8 | 8.02:1 |
| Светлее | oklch(from var(--brand) calc(l + 0.1) c h) | #4484ff | 3.51:1 |
| Вдвое меньше насыщенность | oklch(from var(--brand) l calc(c * 0.5) h) | #4e6faf | 4.98:1 |
| Тон +30° | oklch(from var(--brand) l c calc(h + 30)) | #7a49df | 5.46:1 |
| Дополнительный | oklch(from var(--brand) l c calc(h + 180)) | #956700 | 4.98:1 |
| Серый той же светлоты | oklch(from var(--brand) l 0 h) | #707070 | 4.95:1 |
| Прозрачность 50% | oklch(from var(--brand) l c h / 0.5) | rgba(37, 99, 235, 0.5) | Зависит от фона |
CSS
:root {
--brand: #2563eb;
--brand-hover: oklch(from var(--brand) calc(l - 0.08) c h);
--brand-muted: oklch(from var(--brand) l calc(c * 0.5) h);
--brand-ring: oklch(from var(--brand) l c h / 0.4);
--accent: oklch(from var(--brand) l c calc(h + 30));
}Посмотрите на четыре строки, где l не меняется: при смене насыщенности или тона контраст белого текста гуляет только от 4.95:1 до 5.46:1, у исходного цвета 5.17:1. Контраст определяет светлота, поэтому варианты другого тона, собранные так, сохраняют почти ту же доступность. Светлый и дополнительный цвета при полной насыщенности выходят за охват sRGB, и браузер приводит их к ближайшему цвету, который может показать.
color-mix(), относительные цвета и Sass darken()
color-mix() удобен, когда вы думаете категориями «добавить немного чёрного» или «сделать чуть прозрачнее». Относительный синтаксис удобен, когда вы думаете каналами: «тот же цвет, на 0.1 темнее». Оба заменяют большую часть цветовых функций Sass и следуют за сменой темы без пересборки.
| color-mix() | Относительный синтаксис | Sass darken() | |
|---|---|---|---|
| Где считается | В браузере | В браузере | При сборке |
| Работает с var() и currentColor | Да | Да | Нет |
| Цветовая модель | Любое указанное пространство: srgb, oklab, oklch, hsl и другие | Любая цветовая функция: oklch(), rgb(), hsl(), lab() | Только HSL |
| Для чего | Оттенки, тени, прозрачность, смесь двух токенов | Изменение одного канала: светлоты, насыщенности, тона или альфы | Старый код с фиксированными цветами |
| Размер шага | Доля расстояния до второго цвета | Точный, в единицах канала | Фиксированные пункты HSL, на глаз неровные |
| Поддержка | Chrome 111, Safari 16.2, Firefox 113 | Chrome 119, Safari 18, Firefox 128 | Любой браузер, результат статичный |
Почему Sass darken() даёт неровные шаги
darken($color, 10%) вычитает 10 пунктов светлоты HSL. Светлота HSL не совпадает с воспринимаемой, поэтому заметность изменения зависит от тона. Ниже darken(10%) рядом с oklch(from … calc(l - 0.1) c h) для четырёх ходовых цветов интерфейса. Разница измерена в ΔE OK, где около 0.02 это минимальное различие, которое видно глазом.
| Цвет | Sass darken(10%) | ΔE OK | OKLCH l − 0.1 | ΔE OK |
|---|---|---|---|---|
| #2563eb | #134cca | 0.078 | #0442c8 | 0.099 |
| #facc15 | #d7ad05 | 0.099 | #d9ab00 | 0.101 |
| #16a34a | #107636 | 0.136 | #00842b | 0.094 |
| #dc2626 | #b21d1d | 0.090 | #b70000 | 0.089 |
Один и тот же darken(10%) меняет зелёный в 1.7 раза сильнее, чем синий. Вариант на OKLCH держится между 0.089 и 0.101 для любого тона, а небольшой разброс даёт приведение к охвату: часть затемнённых цветов чуть выходит за sRGB. Сам Sass советует вместо darken() использовать color.adjust() или color.scale(), а в чистом CSS не нужно ни то, ни другое.
Поддержка браузерами и запасные значения
color-mix() работает в Chrome и Edge с версии 111, в Safari с 16.2 и в Firefox с 113, все они вышли в 2023 году. Относительный синтаксис появился в Chrome и Edge 119 и в Firefox 128. Safari выпустил раннюю версию в 16.4, а текущий синтаксис поддерживает с Safari 18. Все актуальные браузеры умеют и то, и другое.
Привычный приём с простым значением строкой выше не работает, если в новом значении есть var(). Объявление с var() при разборе всегда считается корректным, поэтому браузер выбрасывает запасную строку, а если потом не может вычислить color-mix(), берёт унаследованное или начальное значение. Оборачивайте современный вариант в @supports.
CSS
.button:hover {
background: #1f55cc;
}
@supports (color: color-mix(in oklab, red, blue)) {
.button:hover {
background: color-mix(in oklab, var(--brand) 90%, black);
}
}
@supports (color: oklch(from red l c h)) {
.badge {
border-color: oklch(from var(--brand) l c h / 0.3);
}
}Вопросы и ответы
Что делает color-mix() в CSS?
color-mix() смешивает два цвета в указанном цветовом пространстве и возвращает результат, например color-mix(in oklab, var(--brand) 90%, black) даёт цвет на 10% темнее фирменного. Функция считается в браузере, поэтому работает с CSS-переменными и currentColor.
Какое пространство выбрать в color-mix()?
Для оттенков, теней и hover берите oklab: шаги светлоты ровные, тон не уходит. Oklch подходит для смеси двух насыщенных цветов, когда результат тоже должен быть насыщенным, а srgb для смешивания с transparent. Синий с жёлтым в srgb дают плоский серый #808080, а в oklch яркую бирюзу.
Как затемнить цвет при наведении на чистом CSS?
Напишите background: color-mix(in oklab, var(--brand) 90%, black) или background: oklch(from var(--brand) calc(l - 0.1) c h). Для #2563eb первый вариант даёт #1f55cc, второй #0442c8, а контраст белого текста растёт с 5.17:1 до 6.50:1 и 8.02:1.
Как сделать цвет из CSS-переменной полупрозрачным?
Используйте color-mix(in srgb, var(--brand) 20%, transparent) или oklch(from var(--brand) l c h / 0.2). Оба варианта работают, даже если --brand записан в hex, и меняют только непрозрачность, не трогая сам цвет.
Поддерживается ли color-mix() во всех браузерах?
Да, во всех актуальных: в Chrome и Edge с версии 111, в Safari с 16.2 и в Firefox с 113. Для старых браузеров задайте статичный цвет, а вариант с color-mix() поместите в @supports, потому что строка с var() сама откатиться не может.
Чем color-mix() отличается от относительного синтаксиса цвета?
color-mix() смешивает два цвета в заданной пропорции, это удобно для оттенков, теней и прозрачности. Относительный синтаксис, например oklch(from var(--brand) l c calc(h + 30)), переписывает отдельные каналы одного цвета и подходит для точных шагов светлоты, сдвига тона и альфы.