Перейти к подбору цвета

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° и насыщенностью исходных цветов.

ПространствоСиний + жёлтыйOKLCH L, CBlue-600 + yellow-400OKLCH L, C
srgb#8080800.600, 0#9098800.665, 0.035
oklab#6cabc70.710, 0.077#90a3ad0.703, 0.026
oklch#00bdb00.710, 0.262#00bd9e0.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)#1f55cc6.50:1 с белым текстом
Active, на 20% темнееcolor-mix(in oklab, var(--brand) 80%, black)#1947ae8.22:1 с белым текстом
Hover в srgbcolor-mix(in srgb, var(--brand) 90%, black)#2159d36.11:1 с белым текстом
Светлый фон, 10%color-mix(in oklab, var(--brand) 10%, white)#e8f0ff4.51:1 с фирменным текстом
Светлый фон, 20%color-mix(in oklab, var(--brand) 20%, white)#d2e2ff3.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)#0442c88.02:1
Светлееoklch(from var(--brand) calc(l + 0.1) c h)#4484ff3.51:1
Вдвое меньше насыщенностьoklch(from var(--brand) l calc(c * 0.5) h)#4e6faf4.98:1
Тон +30°oklch(from var(--brand) l c calc(h + 30))#7a49df5.46:1
Дополнительныйoklch(from var(--brand) l c calc(h + 180))#9567004.98:1
Серый той же светлотыoklch(from var(--brand) l 0 h)#7070704.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 113Chrome 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 OKOKLCH l − 0.1ΔE OK
#2563eb#134cca0.078#0442c80.099
#facc15#d7ad050.099#d9ab000.101
#16a34a#1076360.136#00842b0.094
#dc2626#b21d1d0.090#b700000.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)), переписывает отдельные каналы одного цвета и подходит для точных шагов светлоты, сдвига тона и альфы.

Попробуйте в PickShade

Похожие статьи