The CSS filter property applies graphical effects, blur, color shifts, shadows, directly to an element and everything it contains, in one line:
.thumbnail {
filter: grayscale(100%);
}That is the whole feature for the common case. filter is Baseline now, unprefixed, on every current engine, and it works on any element: images, video, text, an entire <div>. Back when I first wrote this, filters were a WebKit-nightly toy that could not even touch <video>. None of that is true anymore. The rest of this page is the function-by-function reference, plus the two things people actually get wrong: when drop-shadow() beats box-shadow, and how not to wreck your frame rate.
The filter functions
Each filter function takes one element and returns a modified version of it. The full set:
.demo {
filter: blur(4px); /* gaussian blur, takes a length */
filter: brightness(150%); /* 100% = unchanged, 0% = black */
filter: contrast(80%); /* 100% = unchanged, 0% = grey */
filter: grayscale(100%); /* 0% = original, 100% = fully grey */
filter: saturate(200%); /* 100% = unchanged, 0% = grayscale */
filter: sepia(60%); /* warm photo-tint, 0% to 100% */
filter: hue-rotate(90deg); /* rotate every hue on the wheel */
filter: invert(100%); /* photo negative */
filter: opacity(50%); /* like opacity:, but composites with siblings */
}A few traps in the value ranges. The percentage functions (brightness, contrast, saturate, grayscale, sepia, invert, opacity) also accept a unitless decimal, so grayscale(0.3) and grayscale(30%) are the same thing. What does not work is passing a length where a number belongs: grayscale(3px) is invalid and the whole declaration is dropped silently. blur() is the opposite, it wants a length (blur(4px)), never a number or a percentage. hue-rotate() takes an angle (deg, turn, rad).
brightness and saturate have no upper bound, so brightness(300%) and saturate(400%) are legal and useful for a punchy hover state. opacity() as a filter looks redundant next to the opacity property, and for a single element it usually is, but because it is a filter it can be GPU-composited alongside the others in a chain.
drop-shadow()
drop-shadow() is the function that earns this property its keep, because it does something no other CSS can:
.logo {
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.4));
}The argument list is offset-x offset-y blur-radius color, the same shape as the first four values of box-shadow.
The difference that matters: box-shadow traces the element's bounding box, drop-shadow() traces its actual painted shape. Give a transparent PNG, an SVG icon, or an element clipped with clip-path a box-shadow and you get a rectangle floating behind the visible art. Give it drop-shadow() and the shadow follows the alpha channel: the silhouette of the logo, the points of the star, the curve of the speech bubble. That is the entire reason to reach for it.
Two things drop-shadow() cannot do that box-shadow can. There is no spread value (the fourth box-shadow length), and there is no inset keyword, so it is outer-shadow only. And for the same input values the two render slightly differently, box-shadow reads heavier, so do not expect a pixel match if you swap one for the other.
Chaining multiple filters
Pass a space-separated list and the browser applies them left to right. Order changes the result, this is a pipeline, not a set:
.faded-hero {
filter: grayscale(50%) brightness(0.8) blur(2px);
}
/* a card that pops on hover */
.card {
filter: saturate(80%) brightness(0.95);
transition: filter 200ms ease;
}
.card:hover {
filter: saturate(120%) brightness(1.05);
}Because order matters, blur() then drop-shadow() shadows the already-blurred shape, while drop-shadow() then blur() blurs the shadow too. Decide which you want.
For a dark-mode hack, invert(1) hue-rotate(180deg) flips lightness while leaving hues roughly intact, handy for inverting a diagram or a code screenshot without turning every color into its complement.
backdrop-filter: frosted glass
filter affects the element itself. backdrop-filter affects what is behind it, the pixels showing through a translucent element, which is how you get the frosted-glass panel that is everywhere in modern UI:
.glass-panel {
background: rgba(255, 255, 255, 0.15);
-webkit-backdrop-filter: blur(12px) saturate(160%);
backdrop-filter: blur(12px) saturate(160%);
}The element needs a semi-transparent background, otherwise there is nothing to see the blur through. Two caveats from shipping this in production:
- Keep the
-webkit-prefix. Safari supportedbackdrop-filterfor years only behind-webkit-, dropping the prefix in Safari 18 (2024). Older iOS and macOS traffic still relies on the prefixed form, so ship both: list the prefixed line first, the unprefixed second. - A CSS custom property in the prefixed Safari path does not resolve, so
-webkit-backdrop-filter: blur(var(--blur))quietly fails there. Use a literal value forbackdrop-filterrather than avar().
If you want the panel to degrade gracefully where it is unsupported, gate it on a feature query and keep a solid fallback background, which is the kind of progressive enhancement CSS feature queries with @supports are built for.
Performance: animate sparingly
Filters are GPU-accelerated, but they are not free. blur() in particular is expensive, the browser has to sample a neighborhood of pixels for every output pixel, and backdrop-filter is worse because it re-samples the backdrop on every paint. A static filter on a handful of elements is fine. Animating filter: blur() across a large area, or stacking many blurred glass panels that overlap, is where you watch the frame rate fall off a cliff on a mid-range phone.
Two rules I hold to. First, prefer animating opacity and transform (the two properties the compositor can handle without a repaint) over animating filter when you have the choice. Second, if you must animate a filter, hint it with will-change: filter so the browser promotes the element to its own layer ahead of time, but remove the hint when the animation ends, a permanent will-change wastes memory. And respect motion preferences: gate any filter animation behind @media (prefers-reduced-motion: no-preference).
Quick reference
| Function | Argument | Identity (no change) | Notes |
|---|---|---|---|
blur() | length | blur(0) | length only, never a number |
brightness() | number/% | 100% | no upper bound |
contrast() | number/% | 100% | 0% = solid grey |
grayscale() | number/% | 0% | 100% = fully grey |
saturate() | number/% | 100% | over 100% oversaturates |
sepia() | number/% | 0% | warm photo tint |
hue-rotate() | angle | 0deg | rotates the color wheel |
invert() | number/% | 0% | 100% = negative |
opacity() | number/% | 100% | composites in the filter chain |
drop-shadow() | offsets, blur, color | none | follows the alpha shape; no spread, no inset |
FAQ
See also
- CSS gradient text: clip a gradient to text: another
background-clipand color trick that pairs well with a subtledrop-shadow(). - 8-digit hex colors: alpha transparency in hex: the cleanest way to write the translucent fill a frosted-glass panel needs.
- The CSS :has() parent selector: style a container based on its contents, useful for toggling a filtered state from a wrapper.
- CSS feature queries with @supports: gate
backdrop-filterbehind a feature query with a solid fallback.
Sources
Authoritative references this article was fact-checked against.
- filter, CSS property, MDN Web Docsdeveloper.mozilla.org
- drop-shadow(), CSS function, MDN Web Docsdeveloper.mozilla.org
- backdrop-filter, CSS property, MDN Web Docsdeveloper.mozilla.org
- CSS Backdrop Filter, browser support, Can I usecaniuse.com
- CSS Filter Effects, browser support, Can I usecaniuse.com





