Полупрозрачный 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 и похожие инструменты расставят его сами.

Поддержка backdrop-filter браузерами
chrome
Chrome
76
firefox
Firefox
103
edge
Edge
79
safari
Safari
18
opera
Opera
64
Поддержка mask-image браузерами
chrome
Chrome
120
firefox
Firefox
53
edge
Edge
120
safari
Safari
15.4
opera
Opera
106

Версии в виджетах — для свойств без префикса; с -webkit- оба работают заметно раньше.

Три уровня через @supports

Чтобы хедер оставался читаемым в любом браузере, стили удобно раскладывать по слоям с помощью директивы @supports:

  1. по умолчанию хедер почти непрозрачный — без стекла, но текст читается;
  2. если есть backdrop-filter, фон становится полупрозрачным и включается размытие;
  3. если есть ещё и 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, градиент сверху, кромка и фолбэки — доводка, которую добавляют по необходимости.