Полупрозрачный header, сквозь который размыто проступает контент страницы, — один из самых узнаваемых приёмов современного интерфейса. Собирается он одной строкой: backdrop-filter: blur(16px). Как работает само свойство и какие у него бывают значения, мы разбирали в статье про фильтр фона. Здесь — следующий шаг: почему такое стекло почти всегда выглядит плоским и как несколькими свойствами сделать его похожим на настоящее.
Почему стекло на одном blur() выглядит плоским
У backdrop-filter есть ограничение, о котором редко задумываются: фильтр обрабатывает только те пиксели, которые находятся строго под элементом. Для brightness() или hue-rotate() это логично — каждый пиксель меняется сам по себе. А вот размытие по Гауссу по своей природе смешивает пиксель с соседями, и соседи за границей элемента в расчёт не попадают.
На практике это выглядит так: яркая картинка подъезжает к нижнему краю хедера, но хедер никак на неё не реагирует. Цвет появляется в стекле только тогда, когда картинка уже заехала под него, — резко, без мягкого ореола. Настоящее матовое стекло ведёт себя иначе: свет от предмета рядом рассеивается и подсвечивает стекло заранее.
/* Так делает большинство: размытие видит только то, что под хедером */
.topbar {
position: sticky;
top: 0;
backdrop-filter: blur(16px);
}
Напрямую настроить область, которую учитывает фильтр, нельзя. Поэтому идём в обход: делаем размывающий элемент больше, чем нужно, а лишнее прячем.
Растянуть подложку и отрезать маской
Шаг 1. Выносим размытие в дочерний слой
Задавать самому хедеру фиксированную высоту плохо — она должна подстраиваться под содержимое. Поэтому фильтр переезжает на отдельный пустой элемент внутри него, абсолютно спозиционированный и вдвое выше родителя:
<header class="topbar">
<div class="topbar__glass"></div>
<a href="/" class="topbar__logo">Acme</a>
<nav>…</nav>
</header>
.topbar {
position: relative;
isolation: isolate; /* слой с z-index: -1 остаётся внутри хедера, под логотипом */
}
.topbar__glass {
position: absolute;
inset: 0;
z-index: -1;
height: 200%; /* в 2 раза выше хедера, сколько бы там ни было */
backdrop-filter: blur(16px);
}
z-index: -1 кладёт слой под содержимое хедера: иначе абсолютно спозиционированный элемент нарисуется поверх логотипа и размоет его вместе с фоном. А isolation: isolate не даёт ему провалиться ещё глубже, под сам хедер.
Теперь нижняя половина слоя заходит на контент под хедером, и размытие начинает его учитывать. Осталось эту нижнюю половину скрыть.
Шаг 2. Почему не overflow и не clip-path
Первое, что приходит в голову, — overflow: hidden на хедере. В Firefox и Safari это работает. А в Chrome долгое время ореол от такой обрезки пропадал: там обрезка по границе выполнялась до применения фильтра, и к моменту размытия лишних пикселей уже не было. Так же вели себя overflow: clip и clip-path. В свежих версиях Chrome все три способа уже дают мягкий ореол, но в старых, которые ещё встречаются у пользователей, — нет.
Шаг 3. Маска с резким переходом
Надёжный вариант для всех браузеров — mask-image: маскирование везде выполняется после фильтров. Маска задаёт, какие части элемента видимы: непрозрачные участки маски показывают элемент, прозрачные — прячут. Подробно про маски — в статье CSS маски, а нам хватит градиента:
.topbar__glass {
position: absolute;
inset: 0;
z-index: -1;
height: 200%;
backdrop-filter: blur(16px);
mask-image: linear-gradient(
to bottom,
#000 0 50%,
transparent 50% 100%
);
}
Градиент здесь необычный: плавного перехода нет, цвет сменяется скачком ровно на середине. Это и нужно — верхняя половина слоя видна полностью, нижняя не видна вообще. Цвет #000 выбран не за черноту: маске важна только непрозрачность, white или red дали бы тот же результат.
Откуда 50%: проценты в градиенте считаются от размера самого элемента, а он вдвое выше хедера. Если хедер 64 px, слой получается 128 px, маска показывает его первые 64 px и прячет следующие 64 px. Хотите, чтобы размытие «видело» дальше, — увеличьте высоту слоя и пересчитайте точку перехода: при height: 300% граница маски встанет на 33.33%.
/* Слой в 3 раза выше хедера — граница маски на трети */
.topbar__glass {
height: 300%;
mask-image: linear-gradient(to bottom, #000 0 33.33%, transparent 33.33%);
}
Три побочных эффекта и как их убрать
Невидимый слой перехватывает клики
Маска прячет нижнюю половину слоя визуально, но физически элемент остаётся на месте. Он лежит поверх текста и ссылок под хедером, и они перестают нажиматься и выделяться. Лечится одной строкой — pointer-events: none (подробнее о свойстве — в статье про блокировку событий):
.topbar__glass {
/* …всё, что было выше… */
pointer-events: none;
}
Это ещё одна причина держать стекло отдельным элементом: самому хедеру отключать события нельзя, в нём живут логотип и навигация.
Мерцание у верхнего края окна
Растянутый слой решает проблему снизу, но не сверху. Когда яркий блок уходит за верхний край окна, размытие перестаёт его учитывать — элементы за пределами вьюпорта в расчёт backdrop-filter не попадают, как их ни раскладывай. В узкой полосе у самого верха появляются дёрганые цветные пятна.
Надёжного способа заставить фильтр видеть то, что выше экрана, нет, поэтому пятна просто прикрываем: даём слою фон-градиент от сплошного цвета страницы к прозрачному.
.topbar__glass {
/* …всё, что было выше… */
background: linear-gradient(
to bottom,
#fff 0%, /* здесь — фон вашей страницы */
transparent 50%
);
}
Раньше слой был полностью прозрачным. Теперь у верхнего края он плотный и закрывает мерцание, а к середине растворяется и не мешает эффекту стекла.
Слишком пёстрый фон мешает читать
Если под шапкой текст, размытые строки просвечивают сквозь название сайта и пункты меню, и всё сливается. Стекло нужно сделать «толще», и способов два.
Первый — увеличить радиус размытия, чтобы строки превратились в однородное пятно:
.topbar__glass {
backdrop-filter: blur(28px);
}
Второй — дать самому хедеру полупрозрачный фон, который приглушит всё, что под ним:
.topbar {
background: rgb(255 255 255 / 0.6);
}
Можно было бы добиться того же, сделав градиент из прошлого раздела не полностью прозрачным внизу. Но удобнее держать эти две вещи раздельно: градиент отвечает за верхний край, фон хедера — за плотность, и крутить их можно независимо.
Объём: стеклянная кромка
Настоящая стеклянная панель имеет толщину, и на её торце свет преломляется иначе. Эту иллюзию даёт второй слой: тонкая полоска под хедером со своим, более слабым размытием и повышенной яркостью.
.topbar__edge {
--edge: 4px; /* толщина кромки */
position: absolute;
inset: 0;
z-index: -1;
/* высота как у хедера — чтобы размытие захватывало соседний контент */
height: 100%;
/* сдвигаем на свою высоту вниз, под хедер */
transform: translateY(100%);
background: rgb(255 255 255 / 0.1);
backdrop-filter: blur(8px) brightness(1.2);
pointer-events: none;
/* видны только первые несколько пикселей */
mask-image: linear-gradient(
to bottom,
#000 0 var(--edge),
transparent var(--edge)
);
}
Логика та же, что у основного слоя: элемент намеренно больше видимой части, маска оставляет от него только полоску в --edge пикселей. Радиус размытия у кромки лучше брать меньше, чем у основного стекла (8 px против 16 px), а brightness() выше единицы делает её заметной.
Фильтры в backdrop-filter можно перечислять через пробел, и подбор комбинации — скорее вкус, чем правило. Небольшое повышение насыщенности, например, убирает мутность, которая часто появляется после сильного размытия:
.topbar__glass {
backdrop-filter: blur(12px) brightness(0.9) saturate(1.4);
}
.topbar__edge {
backdrop-filter: blur(6px) brightness(1.1) saturate(1.2);
}
Полный список значений, которые можно комбинировать, — в статье про фильтры в CSS.
Скруглённые углы через SVG-маску
У приёма есть неочевидная цена: border-radius на слое стекла больше не работает как надо. Скругляются углы всего элемента, а нижние углы у него — на невидимой половине, под маской. Для плавающей панели со скруглениями со всех сторон это проблема.
Решение — задать форму маски не градиентом, а SVG-элементом <mask> со скруглённым прямоугольником и сослаться на него по id:
<header class="panel">
<div class="panel__glass"></div>
<svg class="panel__mask" width="100%" height="100%" aria-hidden="true">
<mask id="panel-shape">
<rect width="100%" height="100%" rx="12" ry="12" fill="#fff" />
</mask>
</svg>
…
</header>
.panel {
position: relative;
isolation: isolate;
}
.panel__glass {
position: absolute;
inset: 0;
z-index: -1;
height: 200%;
backdrop-filter: blur(16px);
mask-image: url(#panel-shape);
pointer-events: none;
}
/* SVG повторяет размер панели, а не растянутого слоя */
.panel__mask {
position: absolute;
inset: 0;
z-index: -2;
}
Прямоугольник в SVG занимает 100% размера панели, поэтому маска показывает у слоя стекла ровно область панели с закруглёнными углами, а всё, что ниже, прячет. Вместо <rect> можно нарисовать любую другую фигуру.
Подвох: предок, который ломает размытие
Иногда стекло собрано правильно, а размытия нет вообще или оно не видит фон страницы. Частая причина — кто-то из предков стал так называемым backdrop root, «корнем подложки». Размытие не заглядывает дальше такого предка и обрабатывает только то, что нарисовано внутри него.
Корнем подложки элемент становится, если у него:
- opacity меньше 1;
- filter, backdrop-filter, mask, mask-image или clip-path не none;
- mix-blend-mode не normal;
- will-change с любым из этих свойств.
/* Хедер плавно показывают при загрузке — и стекло перестаёт размывать страницу */
.topbar {
opacity: 0.99;
transition: opacity 0.3s;
}
/* То же самое из-за will-change */
.topbar {
will-change: opacity;
}
Отсюда два практических вывода. Анимировать появление стеклянного хедера лучше через transform, а не через opacity. И маску нельзя переносить на сам хедер — только на слой стекла, иначе хедер станет корнем подложки для кромки. Полный список условий — на MDN.
Поддержка браузерами и фолбэки
Оба свойства давно доступны во всех основных движках. Префикс -webkit- остаётся нужен для Safari до 18-й версии (backdrop-filter) и для Chrome до 120-й (mask-image) — Autoprefixer и похожие инструменты расставят его сами.
Версии в виджетах — для свойств без префикса; с -webkit- оба работают заметно раньше.
Три уровня через @supports
Чтобы хедер оставался читаемым в любом браузере, стили удобно раскладывать по слоям с помощью директивы @supports:
- по умолчанию хедер почти непрозрачный — без стекла, но текст читается;
- если есть backdrop-filter, фон становится полупрозрачным и включается размытие;
- если есть ещё и mask-image, слой растягивается и учитывает соседний контент.
Полный код с этими уровнями — в последнем разделе.
Особенности Firefox
- Долгое время backdrop-filter не срабатывал на элементе с position: sticky, если у одного из предков одновременно заданы overflow и border-radius (баг 1803813). В свежих сборках он уже не воспроизводится, но в старых стабильных версиях ещё может встретиться. Если sticky-хедер в Firefox вдруг стал резким — проверьте эту комбинацию. Про другие ловушки sticky — в статье почему sticky не работает.
- Бывает, что размытие при прокрутке на мгновение пропадает и под стеклом проступают чёткие контуры. Если столкнулись с этим, помогает overscroll-behavior: none на прокручиваемом контейнере.
Уважаем настройку «меньше прозрачности»
В macOS, iOS и Windows есть системная настройка, которая просит приложения уменьшить прозрачность: людям со слабым зрением или с повышенной чувствительностью к визуальному шуму полупрозрачные слои мешают читать. Сайт может узнать о ней через медиа-запрос prefers-reduced-transparency и сделать хедер плотным:
@media (prefers-reduced-transparency: reduce) {
.topbar {
background: rgb(255 255 255 / 0.97);
}
.topbar__glass,
.topbar__edge {
display: none;
}
}
Пока запрос понимают только браузеры на Chromium (с 118-й версии), в Firefox он спрятан за флагом, а Safari его не знает. Но блок ничего не стоит: где запрос не поддерживается, он просто не сработает.
Готовый шаблон
Всё вместе: sticky-хедер с растянутым слоем, кромкой, градиентом против мерцания и тремя уровнями поддержки. Это отправная точка — высоту слоя, радиусы и цвета стоит подогнать под свой дизайн.
<header class="topbar">
<div class="topbar__glass"></div>
<div class="topbar__edge"></div>
<a href="/" class="topbar__logo">Acme</a>
<nav class="topbar__nav">…</nav>
</header>
.topbar {
--edge: 4px;
position: sticky;
top: 0;
z-index: 10;
isolation: isolate; /* слои с z-index: -1 остаются внутри хедера */
/* уровень 1: браузер без backdrop-filter */
background: rgb(255 255 255 / 0.95);
}
/* уровень 2: есть размытие */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
.topbar {
background: rgb(255 255 255 / 0.5);
}
.topbar__glass {
position: absolute;
inset: 0;
z-index: -1;
background: linear-gradient(to bottom, #f2f2f2, transparent 50%);
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
pointer-events: none;
}
.topbar__edge {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
height: var(--edge);
transform: translateY(100%);
background: rgb(255 255 255 / 0.1);
-webkit-backdrop-filter: blur(12px) brightness(0.96);
backdrop-filter: blur(12px) brightness(0.96);
pointer-events: none;
}
}
/* уровень 3: есть маски — размытие учитывает соседний контент */
@supports (mask-image: none) or (-webkit-mask-image: none) {
.topbar__glass {
height: 200%;
-webkit-mask-image: linear-gradient(to bottom, #000 0 50%, transparent 50%);
mask-image: linear-gradient(to bottom, #000 0 50%, transparent 50%);
}
.topbar__edge {
inset: 0;
height: 100%;
-webkit-mask-image: linear-gradient(to bottom, #000 0 var(--edge), transparent var(--edge));
mask-image: linear-gradient(to bottom, #000 0 var(--edge), transparent var(--edge));
}
}
@media (prefers-reduced-transparency: reduce) {
.topbar {
background: rgb(255 255 255 / 0.97);
}
.topbar__glass,
.topbar__edge {
display: none;
}
}
Главная идея, которую стоит вынести: backdrop-filter видит только то, что под элементом, поэтому слой стекла делают больше, чем нужно, и лишнее прячут маской: в отличие от overflow и clip-path, она одинаково работает во всех браузерах. Остальное — pointer-events, градиент сверху, кромка и фолбэки — доводка, которую добавляют по необходимости.
Комментарии (0)