Skip to content

Backdrop filters

Backdrop filter utilities apply graphic effects to the area behind an element, not to the element itself: the content underneath is blurred, brightened or recoloured where the element covers it. They are the building block for frosted glass panels, sticky headers over scrolling content and overlays that soften the page behind a dialog.

  • Two kinds of class. Filter classes (backdrop-blur-md, backdrop-brightness-90, backdrop-hue-rotate-180) only set an internal custom property such as --nu-backdrop-blur. The backdrop-layer class writes the backdrop-filter property that combines all of them. A filter class on its own has no visible effect.
  • Combine freely. Because each filter is its own property, several filter classes stack on one backdrop-layer: backdrop-layer backdrop-blur-md backdrop-brightness-90 blurs and darkens.
  • Presets work alone. backdrop-frosted, backdrop-glass, backdrop-dark-glass, backdrop-crystal and backdrop-matte set backdrop-filter directly and need no backdrop-layer. backdrop-none removes the filter.
  • Transparent element required. The effect shows only through a transparent or semi-transparent background. An element with an opaque background hides the filtered backdrop completely.
Class Sets Needs backdrop-layer
backdrop-layer backdrop-filter composed from the eight --nu-backdrop-* properties
backdrop-blur-{key} --nu-backdrop-blur Yes
backdrop-brightness-{key} --nu-backdrop-brightness Yes
backdrop-contrast-{key} --nu-backdrop-contrast Yes
backdrop-grayscale-{key} --nu-backdrop-grayscale Yes
backdrop-hue-rotate-{key} --nu-backdrop-hue-rotate Yes
backdrop-invert-{key} --nu-backdrop-invert Yes
backdrop-saturate-{key} --nu-backdrop-saturate Yes
backdrop-sepia-{key} --nu-backdrop-sepia Yes
backdrop-frosted, backdrop-glass, backdrop-dark-glass, backdrop-crystal, backdrop-matte backdrop-filter No
backdrop-none backdrop-filter: none No

backdrop-layer emits two rules. The first gives every filter property its identity value, a value that changes nothing, such as blur(0) or brightness(100%). It sits inside :where(), which has no specificity, so any filter class overrides it wherever it appears in the stylesheet. The second rule writes the combined filter:

:where(.backdrop-layer) {
--nu-backdrop-blur: blur(0);
--nu-backdrop-brightness: brightness(100%);
--nu-backdrop-contrast: contrast(100%);
--nu-backdrop-grayscale: grayscale(0);
--nu-backdrop-hue-rotate: hue-rotate(0deg);
--nu-backdrop-invert: invert(0);
--nu-backdrop-saturate: saturate(100%);
--nu-backdrop-sepia: sepia(0);
}
.backdrop-layer {
-webkit-backdrop-filter: var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia);
backdrop-filter: var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia);
}

Every backdrop-filter declaration in core, presets and backdrop-none included, is preceded by the same -webkit-backdrop-filter declaration.

Each filter class replaces one of these values. Filters run in the fixed order of the declaration (blur, brightness, contrast, grayscale, hue rotate, invert, saturate, sepia), whatever order the classes are written in.

In the example below, the lower half of the image is covered by three transparent panels. Only the panels with a filter change what shows through them.

No filter, blur on a layer, blur without a layer
NuvoUI logo with the tagline Human-readable SCSS for modern devs
No filter
backdrop-layer backdrop-blur-md
backdrop-blur-md alone: no effect
<div class="relative overflow-hidden rounded-lg">
<img class="block w-full" src="/images/og-image.png" alt="NuvoUI logo with the tagline Human-readable SCSS for modern devs" />
<div class="absolute inset-x-0 bottom-0 h-50p grid cols-3">
<div class="flex center x-center p-2 text-white text-sm font-bold">No filter</div>
<div class="backdrop-layer backdrop-blur-md flex center x-center p-2 text-white text-sm font-bold">backdrop-layer backdrop-blur-md</div>
<div class="backdrop-blur-md flex center x-center p-2 text-white text-sm font-bold">backdrop-blur-md alone: no effect</div>
</div>
</div>

Every filter has a class for each key of its map and a mixin of the same family. The mixins accept the keys and also custom values (see Keys and custom values).

Filter Class Keys and values
Blur backdrop-blur-{key} none 0, sm 2px, md 4px, lg 8px, xl 16px, 2xl 32px
Brightness backdrop-brightness-{key} none 100%, 75, 90, 110, 125, 150 (percent)
Contrast backdrop-contrast-{key} none 100%, 75, 90, 110, 125, 150 (percent)
Grayscale backdrop-grayscale-{key} none 0, 25, 50, 75, 100 (percent)
Hue rotate backdrop-hue-rotate-{key} 0, 90, 180, 270 (degrees)
Invert backdrop-invert-{key} none 0, 25, 50, 75, 100 (percent)
Saturate backdrop-saturate-{key} none 100%, 50, 75, 125, 150, 200 (percent)
Sepia backdrop-sepia-{key} none 0, 25, 50, 75, 100 (percent)

The none key of each filter (and 0 for hue rotate) is its identity value: it switches one filter off while the others on the same layer stay active. The hue rotate classes are named backdrop-hue-rotate-*, like the backdrop-hue-rotate() mixin.

Combining filters on one layer
NuvoUI logo with the tagline Human-readable SCSS for modern devs
grayscale-100
hue-rotate-180
blur-sm brightness-150
invert-100
<div class="relative overflow-hidden rounded-lg">
<img class="block w-full" src="/images/og-image.png" alt="NuvoUI logo with the tagline Human-readable SCSS for modern devs" />
<div class="absolute inset-0 grid cols-2">
<div class="backdrop-layer backdrop-grayscale-100 flex center x-center p-2 text-white text-sm font-bold">grayscale-100</div>
<div class="backdrop-layer backdrop-hue-rotate-180 flex center x-center p-2 text-white text-sm font-bold">hue-rotate-180</div>
<div class="backdrop-layer backdrop-blur-sm backdrop-brightness-150 flex center x-center p-2 text-white text-sm font-bold">blur-sm brightness-150</div>
<div class="backdrop-layer backdrop-invert-100 flex center x-center p-2 text-white text-sm font-bold">invert-100</div>
</div>
</div>

Presets write a complete backdrop-filter value. They do not read the --nu-backdrop-* properties, so filter classes on the same element do not change them.

Class backdrop-filter
backdrop-frosted blur(8px) saturate(90%) brightness(120%)
backdrop-glass blur(8px) saturate(120%) contrast(90%)
backdrop-dark-glass blur(8px) saturate(180%) brightness(70%) contrast(90%)
backdrop-crystal blur(8px) saturate(150%)
backdrop-matte blur(20px) saturate(90%) brightness(90%)
NuvoUI logo with the tagline Human-readable SCSS for modern devs
frosted
glass
dark-glass
crystal
matte
<div class="relative overflow-hidden rounded-lg">
<img class="block w-full" src="/images/og-image.png" alt="NuvoUI logo with the tagline Human-readable SCSS for modern devs" />
<div class="absolute inset-x-0 bottom-0 h-50p grid cols-5">
<div class="backdrop-frosted flex center x-center p-1 text-white text-sm font-bold">frosted</div>
<div class="backdrop-glass flex center x-center p-1 text-white text-sm font-bold">glass</div>
<div class="backdrop-dark-glass flex center x-center p-1 text-white text-sm font-bold">dark-glass</div>
<div class="backdrop-crystal flex center x-center p-1 text-white text-sm font-bold">crystal</div>
<div class="backdrop-matte flex center x-center p-1 text-white text-sm font-bold">matte</div>
</div>
</div>

Presets come after backdrop-layer in the stylesheet, so a preset on the same element as backdrop-layer wins at the same breakpoint.

backdrop-none sets backdrop-filter: none and returns every --nu-backdrop-* property to its identity value, so a composed filter added later starts from a clean state. Use it with a responsive variant to switch the effect off at a breakpoint.

NuvoUI logo with the tagline Human-readable SCSS for modern devs
backdrop-frosted backdrop-none@md: resize the window
<div class="relative overflow-hidden rounded-lg">
<img class="block w-full" src="/images/og-image.png" alt="NuvoUI logo with the tagline Human-readable SCSS for modern devs" />
<div class="absolute inset-x-0 bottom-0 h-50p backdrop-frosted backdrop-none@md flex center x-center p-2 text-white font-bold">
backdrop-frosted backdrop-none@md: resize the window
</div>
</div>

Every class on this page, including backdrop-layer and the presets, has a responsive variant with an @{breakpoint} suffix. Variants apply from their breakpoint upwards.

NuvoUI logo with the tagline Human-readable SCSS for modern devs
backdrop-blur-sm backdrop-blur-xl@md
<div class="relative overflow-hidden rounded-lg">
<img class="block w-full" src="/images/og-image.png" alt="NuvoUI logo with the tagline Human-readable SCSS for modern devs" />
<div class="absolute inset-x-0 bottom-0 h-50p backdrop-layer backdrop-blur-sm backdrop-blur-xl@md flex center x-center p-2 text-white font-bold">
backdrop-blur-sm backdrop-blur-xl@md
</div>
</div>

The identity values of backdrop-layer@md also sit in :where(), so adding backdrop-layer@md does not reset a filter that a base class such as backdrop-blur-md has set.

SuffixMedia query
@xs@media (min-width: 480px)
@sm@media (min-width: 640px)
@md@media (min-width: 768px)
@lg@media (min-width: 1024px)
@xl@media (min-width: 1280px)
@2xl@media (min-width: 1536px)

There are no state classes such as hover:backdrop-blur-md. The mixins accept state modifiers inside apply():

.card {
@include apply(backdrop-layer, backdrop-blur(sm), backdrop-blur(lg) hover);
}

Each filter has a mixin that sets its --nu-backdrop-* property. Include backdrop-layer first: it resets every property to its identity value, so a filter mixin written before it in the same rule is overwritten.

@use "@nuvoui/core" as *;
.overlay {
@include apply(backdrop-layer, backdrop-blur(lg), backdrop-saturate(150));
}
.overlay {
--nu-backdrop-blur: blur(0);
--nu-backdrop-brightness: brightness(100%);
--nu-backdrop-contrast: contrast(100%);
--nu-backdrop-grayscale: grayscale(0);
--nu-backdrop-hue-rotate: hue-rotate(0deg);
--nu-backdrop-invert: invert(0);
--nu-backdrop-saturate: saturate(100%);
--nu-backdrop-sepia: sepia(0);
-webkit-backdrop-filter: var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia);
backdrop-filter: var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia);
--nu-backdrop-blur: blur(8px);
--nu-backdrop-saturate: saturate(150%);
}
Mixin Output
backdrop-layer The eight identity values and the composed backdrop-filter
backdrop-blur($value) and the other seven filter mixins One --nu-backdrop-* property
backdrop-frosted, backdrop-glass, backdrop-dark-glass, backdrop-crystal, backdrop-matte backdrop-filter with the preset value
backdrop-none backdrop-filter: none and the eight identity values
backdrop-filter($value) backdrop-filter with the value as written

The filter mixins are backdrop-blur, backdrop-brightness, backdrop-contrast, backdrop-grayscale, backdrop-hue-rotate, backdrop-invert, backdrop-saturate and backdrop-sepia.

A filter mixin accepts a key of its map, as an unquoted or quoted number (backdrop-brightness(90) and backdrop-brightness("90") both give brightness(90%)). Any other value must carry the unit of that filter:

Mixin Custom values
backdrop-blur A length in px, rem or em
backdrop-hue-rotate An angle in deg, rad, grad or turn
All others A percentage

Every filter mixin also accepts a unitless 0, calc() and var().

.sheet {
@include apply(backdrop-layer, backdrop-blur(6px), backdrop-brightness(80%), backdrop-hue-rotate(45deg));
}
.themed {
@include apply(backdrop-layer, backdrop-blur(var(--sheet-blur)));
}

For a filter list that the layer cannot express, such as a filter twice or a different order, use backdrop-filter(). It writes the value as given, without validation, and replaces any composed or preset filter.

.overlay {
@include backdrop-filter(blur(6px) saturate(140%));
}

The value maps ($backdrop-blur-values, $backdrop-brightness-values, $backdrop-contrast-values, $backdrop-grayscale-values, $backdrop-hue-rotate-values, $backdrop-invert-values, $backdrop-saturate-values, $backdrop-sepia-values) and $backdrop-filter-presets are configurable through @use ... with. Each map replaces the default; it is not merged.

@use "@nuvoui/core" with (
$generate-utility-classes: true,
$backdrop-blur-values: ("none": 0, "sm": 3px, "md": 6px, "lg": 12px)
);

The backdrops feature flag turns off every class on this page. The mixins stay available.

@use "@nuvoui/core" with (
$generate-utility-classes: true,
$feature-flags: ("backdrops": false)
);

generate-backdrop-filter-utilities($breakpoint: null) emits the full class set, with an @{breakpoint} suffix when a breakpoint is given. Call it at the top level, or inside the matching media query for a suffixed set; inside a rule, the classes are nested under that rule’s selector.

@media (min-width: 1800px) {
@include generate-backdrop-filter-utilities(3xl);
}

Filter mixins fail at compile time instead of emitting a filter that the browser would drop.

Input Error
backdrop-blur(7) backdrop-blur(7): use a key (none, sm, md, lg, xl, 2xl), a value in px, rem, em, calc() or var().
backdrop-blur(huge) backdrop-blur(huge): use a key (none, sm, md, lg, xl, 2xl), a value in px, rem, em, calc() or var().
backdrop-brightness(80px) backdrop-brightness(80px): use a key (none, 75, 90, 110, 125, 150), a value in %, calc() or var().
backdrop-saturate(1.5) backdrop-saturate(1.5): use a key (none, 50, 75, 125, 150, 200), a value in %, calc() or var().
backdrop-hue-rotate(45) backdrop-hue-rotate(45): use a key (0, 90, 180, 270), a value in deg, rad, grad, turn, calc() or var().
// error
.panel {
@include backdrop-saturate(1.5);
}

Core emits every backdrop-filter declaration twice, first as -webkit-backdrop-filter and then as the standard property, so browsers that only understand the prefixed property get the same filter. It does not wrap the classes in @supports. Browsers that support neither ignore both, and the element shows its own background over an unfiltered backdrop.

Design for the unfiltered case as well: give the element a background colour that keeps its content readable without the blur, and use @supports (backdrop-filter: blur(1px)) to make that background more transparent only where the filter works.

.site-header {
background-color: token(bg-surface);
@supports (backdrop-filter: blur(1px)) {
background-color: transparent;
@include apply(backdrop-frosted);
}
}

The --nu-backdrop-* properties are internal utility properties, not tokens: token() does not accept them and they do not change with the theme.

Variable Set by Identity value
--nu-backdrop-blur backdrop-blur-*, backdrop-blur() blur(0)
--nu-backdrop-brightness backdrop-brightness-*, backdrop-brightness() brightness(100%)
--nu-backdrop-contrast backdrop-contrast-*, backdrop-contrast() contrast(100%)
--nu-backdrop-grayscale backdrop-grayscale-*, backdrop-grayscale() grayscale(0)
--nu-backdrop-hue-rotate backdrop-hue-rotate-*, backdrop-hue-rotate() hue-rotate(0deg)
--nu-backdrop-invert backdrop-invert-*, backdrop-invert() invert(0)
--nu-backdrop-saturate backdrop-saturate-*, backdrop-saturate() saturate(100%)
--nu-backdrop-sepia backdrop-sepia-*, backdrop-sepia() sepia(0)

These properties are not registered with @property, so they inherit. A backdrop-layer resets all eight on its own element, which keeps a filter set on an ancestor from leaking into a nested layer.

Generated from the compiled core build. Responsive variants are not listed. The :where(.backdrop-layer) identity rule shown in The backdrop layer is not part of this table.

Layer and none (2 rules)
SelectorDeclarationsDefault value
.backdrop-layer-webkit-backdrop-filter: var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia)
backdrop-filter: var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia)
var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia)
var(--nu-backdrop-blur) var(--nu-backdrop-brightness) var(--nu-backdrop-contrast) var(--nu-backdrop-grayscale) var(--nu-backdrop-hue-rotate) var(--nu-backdrop-invert) var(--nu-backdrop-saturate) var(--nu-backdrop-sepia)
.backdrop-none-webkit-backdrop-filter: none
backdrop-filter: none
--nu-backdrop-blur: blur(0)
--nu-backdrop-brightness: brightness(100%)
--nu-backdrop-contrast: contrast(100%)
--nu-backdrop-grayscale: grayscale(0)
--nu-backdrop-hue-rotate: hue-rotate(0deg)
--nu-backdrop-invert: invert(0)
--nu-backdrop-saturate: saturate(100%)
--nu-backdrop-sepia: sepia(0)
none
none
blur(0)
brightness(100%)
contrast(100%)
grayscale(0)
hue-rotate(0deg)
invert(0)
saturate(100%)
sepia(0)
Blur (6 rules)
SelectorDeclarationsDefault value
.backdrop-blur-none--nu-backdrop-blur: blur(0)blur(0)
.backdrop-blur-sm--nu-backdrop-blur: blur(2px)blur(2px)
.backdrop-blur-md--nu-backdrop-blur: blur(4px)blur(4px)
.backdrop-blur-lg--nu-backdrop-blur: blur(8px)blur(8px)
.backdrop-blur-xl--nu-backdrop-blur: blur(16px)blur(16px)
.backdrop-blur-2xl--nu-backdrop-blur: blur(32px)blur(32px)
Brightness (6 rules)
SelectorDeclarationsDefault value
.backdrop-brightness-none--nu-backdrop-brightness: brightness(100%)brightness(100%)
.backdrop-brightness-75--nu-backdrop-brightness: brightness(75%)brightness(75%)
.backdrop-brightness-90--nu-backdrop-brightness: brightness(90%)brightness(90%)
.backdrop-brightness-110--nu-backdrop-brightness: brightness(110%)brightness(110%)
.backdrop-brightness-125--nu-backdrop-brightness: brightness(125%)brightness(125%)
.backdrop-brightness-150--nu-backdrop-brightness: brightness(150%)brightness(150%)
Contrast (6 rules)
SelectorDeclarationsDefault value
.backdrop-contrast-none--nu-backdrop-contrast: contrast(100%)contrast(100%)
.backdrop-contrast-75--nu-backdrop-contrast: contrast(75%)contrast(75%)
.backdrop-contrast-90--nu-backdrop-contrast: contrast(90%)contrast(90%)
.backdrop-contrast-110--nu-backdrop-contrast: contrast(110%)contrast(110%)
.backdrop-contrast-125--nu-backdrop-contrast: contrast(125%)contrast(125%)
.backdrop-contrast-150--nu-backdrop-contrast: contrast(150%)contrast(150%)
Grayscale (5 rules)
SelectorDeclarationsDefault value
.backdrop-grayscale-none--nu-backdrop-grayscale: grayscale(0)grayscale(0)
.backdrop-grayscale-25--nu-backdrop-grayscale: grayscale(25%)grayscale(25%)
.backdrop-grayscale-50--nu-backdrop-grayscale: grayscale(50%)grayscale(50%)
.backdrop-grayscale-75--nu-backdrop-grayscale: grayscale(75%)grayscale(75%)
.backdrop-grayscale-100--nu-backdrop-grayscale: grayscale(100%)grayscale(100%)
Hue rotate (4 rules)
SelectorDeclarationsDefault value
.backdrop-hue-rotate-0--nu-backdrop-hue-rotate: hue-rotate(0deg)hue-rotate(0deg)
.backdrop-hue-rotate-90--nu-backdrop-hue-rotate: hue-rotate(90deg)hue-rotate(90deg)
.backdrop-hue-rotate-180--nu-backdrop-hue-rotate: hue-rotate(180deg)hue-rotate(180deg)
.backdrop-hue-rotate-270--nu-backdrop-hue-rotate: hue-rotate(270deg)hue-rotate(270deg)
Invert (5 rules)
SelectorDeclarationsDefault value
.backdrop-invert-none--nu-backdrop-invert: invert(0)invert(0)
.backdrop-invert-25--nu-backdrop-invert: invert(25%)invert(25%)
.backdrop-invert-50--nu-backdrop-invert: invert(50%)invert(50%)
.backdrop-invert-75--nu-backdrop-invert: invert(75%)invert(75%)
.backdrop-invert-100--nu-backdrop-invert: invert(100%)invert(100%)
Saturate (6 rules)
SelectorDeclarationsDefault value
.backdrop-saturate-none--nu-backdrop-saturate: saturate(100%)saturate(100%)
.backdrop-saturate-50--nu-backdrop-saturate: saturate(50%)saturate(50%)
.backdrop-saturate-75--nu-backdrop-saturate: saturate(75%)saturate(75%)
.backdrop-saturate-125--nu-backdrop-saturate: saturate(125%)saturate(125%)
.backdrop-saturate-150--nu-backdrop-saturate: saturate(150%)saturate(150%)
.backdrop-saturate-200--nu-backdrop-saturate: saturate(200%)saturate(200%)
Sepia (5 rules)
SelectorDeclarationsDefault value
.backdrop-sepia-none--nu-backdrop-sepia: sepia(0)sepia(0)
.backdrop-sepia-25--nu-backdrop-sepia: sepia(25%)sepia(25%)
.backdrop-sepia-50--nu-backdrop-sepia: sepia(50%)sepia(50%)
.backdrop-sepia-75--nu-backdrop-sepia: sepia(75%)sepia(75%)
.backdrop-sepia-100--nu-backdrop-sepia: sepia(100%)sepia(100%)
Presets (5 rules)
SelectorDeclarationsDefault value
.backdrop-frosted-webkit-backdrop-filter: blur(8px) saturate(90%) brightness(120%)
backdrop-filter: blur(8px) saturate(90%) brightness(120%)
blur(8px) saturate(90%) brightness(120%)
blur(8px) saturate(90%) brightness(120%)
.backdrop-glass-webkit-backdrop-filter: blur(8px) saturate(120%) contrast(90%)
backdrop-filter: blur(8px) saturate(120%) contrast(90%)
blur(8px) saturate(120%) contrast(90%)
blur(8px) saturate(120%) contrast(90%)
.backdrop-dark-glass-webkit-backdrop-filter: blur(8px) saturate(180%) brightness(70%) contrast(90%)
backdrop-filter: blur(8px) saturate(180%) brightness(70%) contrast(90%)
blur(8px) saturate(180%) brightness(70%) contrast(90%)
blur(8px) saturate(180%) brightness(70%) contrast(90%)
.backdrop-crystal-webkit-backdrop-filter: blur(8px) saturate(150%)
backdrop-filter: blur(8px) saturate(150%)
blur(8px) saturate(150%)
blur(8px) saturate(150%)
.backdrop-matte-webkit-backdrop-filter: blur(20px) saturate(90%) brightness(90%)
backdrop-filter: blur(20px) saturate(90%) brightness(90%)
blur(20px) saturate(90%) brightness(90%)
blur(20px) saturate(90%) brightness(90%)
  • Check contrast against the busiest backdrop. Text on a blurred panel sits on whatever scrolls or shows behind it, so its contrast changes with the content. Test the lightest and darkest content that can appear behind the text, and add a semi-transparent background colour behind the text when the blur alone does not reach 4.5:1 for body text or 3:1 for large text (WCAG 1.4.3).
  • More blur is more stable. A small blur keeps edges and text of the backdrop recognisable, which competes with the foreground text. Larger keys (lg and up) or a preset give a calmer, more even surface.
  • Readable without the filter. Where the filter is not supported or not rendered, the panel shows its plain background. Make sure the content is still readable then (see Browser support).
  • Reduced transparency. Some users ask the operating system for less transparency. Core has no mixin for the prefers-reduced-transparency media query; in your own CSS, replace the glass effect with an opaque background under @media (prefers-reduced-transparency: reduce).
  • Performance. Backdrop filters are rendered on every frame in which the content behind them changes. Large filtered areas over scrolling or animated content can drop frames on low-end devices; keep the filtered area as small as the design allows and avoid animating the blur radius.
  • RTL. Backdrop filters do not depend on text direction.