Ситуация, знакомая всем, кто делал сайт с редакторской вёрсткой. В герой-блоке лежит фотография, поверх неё — белый логотип и заголовок. На первой картинке всё прекрасно: небо тёмное, белое читается. Потом контент-менеджер загружает снимок заснеженного поля, и логотип исчезает. Классический ответ — накинуть поверх фото полупрозрачную чёрную плашку. Работает, но заодно гасит саму фотографию: её выбирали не для того, чтобы залить серым.
Есть другой путь — не притемнять фон, а заставить сам текст реагировать на то, что под ним. В CSS для этого есть mix-blend-mode, и это тот случай, когда одно свойство закрывает задачу, под которую обычно пишут скрипт с анализом яркости картинки.
Что вообще происходит, когда браузер смешивает два слоя
Смешивание — это поканальная функция от двух чисел: цвета подложки (в спецификации он называется Cb, backdrop) и цвета накладываемого слоя (Cs, source). Оба нормализованы к диапазону от 0 до 1, функция считается отдельно для красного, зелёного и синего. Двенадцать режимов из шестнадцати устроены именно так — их называют разделяемыми (separable), потому что каналы считаются независимо. normal тривиален (берётся верхний цвет), у soft-light формула кусочная и в строку таблицы не помещается, остальные десять выглядят так:
| Режим | Формула из спецификации |
|---|---|
multiply |
B = Cb × Cs |
screen |
B = 1 − (1 − Cb) × (1 − Cs) |
darken |
B = min(Cb, Cs) |
lighten |
B = max(Cb, Cs) |
color-dodge |
B = min(1, Cb / (1 − Cs)) |
color-burn |
B = 1 − min(1, (1 − Cb) / Cs) |
hard-light |
multiply или screen в зависимости от того, темнее ли Cs половины |
overlay |
то же, что hard-light, но слои поменяны местами |
difference |
B = abs(Cb − Cs) |
exclusion |
B = Cb + Cs − 2 × Cb × Cs |
Оставшиеся четыре — hue, saturation, color, luminosity — неразделяемые: они разбирают цвет на тон, насыщенность и светлоту и берут разные составляющие из разных слоёв. color, например, берёт тон и насыщенность сверху, а светлоту снизу — именно поэтому им красят чёрно-белые фотографии.
Проще один раз увидеть все шестнадцать на одной и той же картинке. Ниже белое слово #fff лежит поверх одного и того же файла, меняется только mix-blend-mode:
Рендер в Chromium 141, разметка и цвет текста во всех плитках одинаковые
В плитках multiply, darken и color-burn текст исчез не по ошибке: для всех трёх белый — нейтральный элемент, формула сводится к B = Cb, то есть к подложке без изменений. У hue, saturation и color причина другая: белый источник не несёт ни тона, ни насыщенности, поэтому на выходе остаётся подложка, у которой отобрали цвет, — на светлых участках разница почти неразличима. Если режим «не работает», первым делом смотрите не на браузер, а на то, не подали ли вы на вход нейтральный цвет.
Почему из шестнадцати для контраста берут два
Идею, с которой обычно начинают знакомство с темой, коротко сформулировал Cody Lindsay Gordon: вместо подложек поставить логотипу mix-blend-mode: exclusion, и он сам подстроится под яркость фотографии.
.blend-me {
mix-blend-mode: exclusion;
}
Механика видна прямо из формул. У difference при белом источнике B = |Cb − 1| = 1 − Cb: тёмный фон даёт светлый текст, светлый фон — тёмный. Инверсия происходит сама, без единой строчки JavaScript и без анализа гистограммы. У exclusion результат тот же по знаку, но мягче в полутонах — картинка под текстом остаётся видна лучше.
Важная деталь из той же заметки, на которой спотыкаются первым делом: слой, с которым вы хотите смешаться, должен лежать под элементом и быть ему соседом, а не родителем.
<h1 class="blend-me">Hello world</h1>
<figure>
<img src="/hero.jpg" alt="">
</figure>
И где эти два режима ломаются
Теперь неприятное. Подставьте в формулу difference фон ровно посередине шкалы: |0.5 − 1| = 0.5. Результат совпал с подложкой — текст растворился. У exclusion то же самое: 0.5 + 1 − 2 × 0.5 × 1 = 0.5. Оба режима дают ноль контраста ровно там, где фотография средне-серая, а это самая частая яркость у реальных снимков.
Одна и та же надпись поверх шкалы яркости от 0 % до 100 %, рендер в Chromium 141
Ставить mix-blend-mode: difference на основной текст интерфейса — и считать вопрос контраста закрытым. WCAG требует 4.5:1 для обычного текста и 3:1 для крупного (от 24 px, или от 18.66 px полужирным). Результат смешивания зависит от картинки, которую загрузит редактор, поэтому гарантировать эти цифры заранее нельзя в принципе. Проверять «на глаз на одной фотографии» — не проверка.
Отсюда практическое правило: смешивание отлично работает для крупной декоративной типографики, монограмм, цифр, разделителей и наведения — то есть там, где потеря читаемости не ломает сценарий. Для подписей, кнопок и абзацев нужна предсказуемая подложка.
Матрица выбора: какой режим под какую задачу
| Задача | Режим | Что учесть |
|---|---|---|
| Крупный логотип или заголовок поверх произвольного фото | difference, exclusion |
провал контраста на средне-серых участках |
| Затемнить картинку под белый текст | multiply с тёмным цветом |
предсказуемо, но гасит саму фотографию |
| Осветлить картинку под тёмный текст | screen со светлым цветом |
съедает детали в тенях |
| Дуотон, единый стиль всех обложек | color или luminosity |
считается один раз, можно закешировать в CSS |
| Наложить текстуру шума или бумаги | multiply, soft-light |
держите текстуру мелкой, её вес идёт в трафик |
| Свечение, вспышки, частицы | screen, plus-lighter |
plus-lighter есть в Chromium 141, plus-darker — нет |
| Перекрасить чёрно-белую иконку | color |
на цветном источнике результат непредсказуем |
| Подсветить область под курсором | difference на элементе-курсоре |
свойство не анимируется, анимируйте позицию |
mix-blend-mode не анимируется — в MDN так и написано: Animation type: Not animatable. Переключать его в @keyframes бессмысленно; анимируют то, что лежит под ним и над ним, — положение, размер, цвет.
Ловушка первая: элемент смешивается не со всей страницей
Самая частая жалоба звучит как «у меня то же самое, но не работает». Почти всегда причина одна: смешивание идёт не со всем, что нарисовано ниже, а только с содержимым ближайшего контекста наложения (stacking context). Стоит между текстом и картинкой появиться обёртке, которая создаёт свой контекст, — и смешиваться становится не с чем.
А контекст наложения создаёт куда больше свойств, чем помнят наизусть: transform, filter, opacity меньше единицы, will-change, position: fixed, contain: paint, элемент внутри flex- или grid-контейнера с z-index, отличным от auto. Сам mix-blend-mode тоже создаёт контекст — на том элементе, где он объявлен.
Разметка во всех трёх карточках одинаковая, отличается одно свойство на обёртке. Рендер в Chromium 141
Есть и обратная беда. Если контекст не создать вообще, элемент будет смешиваться со всем, что оказалось под ним ниже по дереву, — вплоть до фона страницы и соседних блоков. Лечится это одной строкой на общем родителе:
.hero {
isolation: isolate;
}
isolation: isolate принудительно создаёт контекст наложения и ограничивает смешивание содержимым этого блока. Свойство поддерживается всеми браузерами с января 2020 года.
Отдельная неочевидная история — фон на body. Фон корневого элемента и body браузер продвигает на канву окна, и в смешивании он не участвует: элемент с mix-blend-mode будет смешиваться с прозрачностью, то есть визуально ни с чем. Обложка к этой статье собрана именно так, как надо: фотография лежит на внутреннем блоке-сцене, у которого стоит isolation: isolate, а заголовок над ней идёт с difference.
Ловушка вторая: background-blend-mode решает другую задачу
Похожее название, другая область действия. mix-blend-mode смешивает элемент с тем, что нарисовано под ним. background-blend-mode смешивает между собой слои фона одного и того же элемента — несколько background-image и background-color под ними. Никаких дополнительных обёрток и псевдоэлементов для затемнения не нужно.
Во всех четырёх карточках один и тот же файл в
background-image, рендер в Chromium 141
Дуотон целиком помещается в четыре строки:
.duotone {
background-image: linear-gradient(135deg, #ff2d6f, #00d4ff), url("/hero.jpg");
background-blend-mode: color;
background-size: cover;
background-position: center;
}
Верхний слой отдаёт тон и насыщенность, нижний — светлоту, на выходе получается однородная стилистика для всей ленты обложек. Порядок слоёв в background-image — сверху вниз, то есть градиент идёт первым, а фотография последней.
Затемнение под белый текст без единого лишнего узла в разметке:
.hero {
background-image: url("/hero.jpg");
background-color: #1b2440;
background-blend-mode: multiply;
background-size: cover;
}
Дальше меняете только background-color — и получаете любую степень притемнения без второго div, без ::after и без правок HTML.
Что положить в продакшн
Рабочая схема выглядит так: предсказуемая подложка как база, смешивание — как улучшение сверху, и проверка поддержки через @supports.
.hero {
position: relative;
isolation: isolate;
background: url("/hero.jpg") center / cover;
}
/* база: работает всегда */
.hero__title {
color: #fff;
text-shadow: 0 1px 3px rgb(0 0 0 / 0.75);
}
.hero::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to top, rgb(0 0 0 / 0.55), transparent 60%);
}
/* улучшение: только для крупного декоративного заголовка */
@supports (mix-blend-mode: difference) {
.hero__logo {
mix-blend-mode: difference;
color: #fff;
}
}
Логотип получает автоматическую инверсию, а текст, который обязан читаться, опирается на градиентную подложку и тень — их контраст можно посчитать заранее и проверить любым чекером. Проверять готовый герой-блок стоит минимум на четырёх картинках: очень тёмной, очень светлой, средне-серой и пёстрой — именно средне-серая ловит большинство провалов.
Источники
- MDN: mix-blend-mode — список значений, формальный синтаксис, заметка о контексте наложения
- MDN: blend-mode — описание каждого из шестнадцати режимов
- MDN: isolation — значения и статус поддержки
- W3C, Compositing and Blending Level 1 — формулы разделяемых режимов и определение изолированной группы
- Cody Lindsay Gordon, Contrast Logos & Text with Background Photos Using CSS Blend Modes — исходная идея с
exclusionи требование к порядку элементов
Смежное по теме на форуме: CSS в письме читает пароли: разбор исследования PortSwigger и Пила на нулевой громкости: скрытый AudioContext на AliExpress.
Пускали mix-blend-mode в боевой проект — или обошлись градиентной подложкой? И был ли случай, когда режим вёл себя по-разному в Chromium и в Safari: интересно собрать список расхождений, которые ловили руками.




