CSS filter and backdrop-filter: blur, brightness, drop-shadow and more
A complete guide to every CSS filter function — blur, brightness, contrast, grayscale, hue-rotate, invert, saturate, sepia, drop-shadow — plus backdrop-filter for glassmorphism. Live demos included.
CSS filter brings Photoshop-grade image effects directly into the browser — no canvas, no JavaScript, no image editing pipeline. Blur, color shift, brightness, duotone: all rendered in real time by the GPU. And with backdrop-filter, you can apply those same effects to whatever is behind an element, which is how glassmorphism actually works.
This guide covers every filter function, when to combine them, the critical difference between filter and backdrop-filter, real-world use cases with live demos, and the performance traps worth knowing before you ship.
What are CSS filters?
The filter property applies graphical effects to an element and all of its descendants. It uses the same effect functions as SVG filters but with a simpler, CSS-native syntax. Every filter function has a "no-op" default — the value that produces no visible change — which is useful to remember when building transitions.
The syntax is:
filter: function1(value) function2(value) ...; Multiple functions are separated by spaces and applied left to right. Unlike most CSS properties, you cannot split them across multiple declarations — a second filter: overwrites the first.
What does each CSS filter function do?
Here is every function in the specification, applied to the same gradient so you can compare them directly.
blur(radius)
Applies a Gaussian blur. The radius is in pixels or other length units — 0 produces no blur (the default). There is no maximum, but values above ~20px are rarely useful and increasingly expensive. Use blur() for loading skeletons, depth-of-field effects, or revealing content on hover.
filter: blur(0); /* default — no blur */
filter: blur(4px); /* soft blur */
filter: blur(20px); /* heavy blur, good for skeleton screens */ brightness(amount)
Values below 1 darken, values above 1 brighten. 0 = pure black, 2 = twice as bright. The default is 1. Percentages also work: brightness(50%) = brightness(0.5).
filter: brightness(1); /* default */
filter: brightness(0.5); /* 50% darker */
filter: brightness(1.5); /* 50% brighter */ contrast(amount)
0 = flat grey, 1 = unchanged, values above 1 increase contrast. Combining contrast(1.1) with brightness(1.05) is a classic photo enhancement combo.
grayscale(amount)
0 = full color (default), 1 or 100% = fully desaturated. Intermediate values create partial desaturation. Great for "disabled" states or hover effects that reveal color.
hue-rotate(angle)
Rotates all hues around the color wheel. 0deg = unchanged, 180deg = opposite hues, 360deg = back to start. Useful for color theming without changing images: apply hue-rotate() to a container to shift its entire color palette.
filter: hue-rotate(0deg); /* unchanged */
filter: hue-rotate(90deg); /* shifted +90° on color wheel */
filter: hue-rotate(180deg); /* complementary colors */ invert(amount)
0 = unchanged, 1 or 100% = fully inverted (photo negative effect). Useful for dark mode image adjustments: filter: invert(1) hue-rotate(180deg) inverts an image while largely preserving its perceived colors.
opacity(amount)
Functionally similar to the opacity property, but with one important difference: filter: opacity() always creates a new compositing layer, which can improve animation smoothness but increases GPU memory. For static transparency, use the opacity property instead.
saturate(amount)
0 = fully desaturated (like grayscale), 1 = unchanged, values above 1 increase saturation. saturate(3) makes colors vivid and electric. Values below 1 create a muted, pastel effect.
sepia(amount)
0 = unchanged, 1 or 100% = full warm brown sepia tone (vintage photo look). Intermediate values create a subtle warm color cast.
drop-shadow(offset-x offset-y blur color)
Unlike box-shadow, drop-shadow follows the actual alpha channel of the element — so on a transparent PNG or an SVG, it traces the real shape instead of the bounding box. It does not support spread-radius or inset.
filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.6)); (follows bounding box)
(follows actual shape)
The difference is visible on any non-rectangular element. Try the drop-shadow generator to build the syntax and copy it instantly.
How do you combine multiple CSS filter functions?
List them space-separated in a single filter declaration. Order matters: brightness(2) grayscale(1) brightens first, then desaturates — the grayscale sees a brighter input. Reversing the order can produce slightly different results with some function combinations.
/* Classic photo enhancement — "Instagram-style" */
filter: brightness(1.05) contrast(1.1) saturate(1.25);
/* Vintage photo */
filter: sepia(0.6) contrast(1.1) brightness(0.9);
/* Duotone: warm */
filter: sepia(1) hue-rotate(330deg) saturate(4);
/* Dark mode image invert (preserves perceived colors) */
filter: invert(1) hue-rotate(180deg); Live: filter combinations
Try it live: The CSS filter generator lets you combine all filter functions with sliders and copy the final declaration in one click.
What is backdrop-filter and how is it different from filter?
filter applies effects to the element and everything inside it. backdrop-filter applies effects to the area behind the element — everything rendered beneath it in the stacking context.
For backdrop-filter to be visible, the element's background must have some transparency. A solid background: #fff would hide the blurred backdrop entirely.
/* filter — blurs the element itself */
.card { filter: blur(8px); }
/* backdrop-filter — blurs what's behind the element */
.glass-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); /* Safari */
} What is glassmorphism and how do you build it?
Glassmorphism is a UI style that mimics frosted glass — a panel with a slightly transparent background, a blur of the content behind it, and a subtle border. Three CSS properties make it work: backdrop-filter: blur(), background: rgba(), and border: 1px solid rgba().
.glass {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px); /* Safari */
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 12px;
}
/* Fallback when backdrop-filter is unsupported */
@supports not (backdrop-filter: blur(1px)) {
.glass {
background: rgba(15, 15, 20, 0.85);
}
} The @supports rule is important. Without backdrop-filter, the nearly-transparent background makes text unreadable. The fallback increases opacity to ensure legibility regardless of what's behind the card.
How do you build a grayscale-to-color hover effect?
One of the most common filter use cases: images or elements that appear desaturated until hovered. With a CSS transition on the filter property, this is three lines of CSS.
.card-image {
filter: grayscale(100%);
transition: filter 0.4s ease;
}
.card-image:hover {
filter: grayscale(0%);
} The same pattern works in reverse for "focus" effects: full color by default, muted on hover to reduce visual noise in dense UI.
What is the duotone effect and how do you achieve it with CSS filters?
A duotone effect maps an image to two colors — a shadow color and a highlight color. With pure CSS, you can approximate it by chaining sepia(), hue-rotate(), and saturate(). This shifts the image into a sepia tone and then rotates and amplifies that hue.
/* Purple duotone */
filter: sepia(1) hue-rotate(200deg) saturate(5);
/* Gold duotone */
filter: sepia(1) hue-rotate(330deg) saturate(4);
/* Cyan duotone */
filter: sepia(1) hue-rotate(130deg) saturate(5); This is a CSS approximation — a true duotone requires blending modes or SVG filters for precise color control. But for UI accents and hero backgrounds it is fast and requires no image assets.
How do you use CSS blur() as a skeleton loader?
Blurring content while it loads — then transitioning to clear — is a progressively enhanced loading pattern that avoids layout shift and looks better than placeholder greys.
.content {
filter: blur(8px);
transition: filter 0.5s ease;
}
.content.loaded {
filter: blur(0);
} Apply a class via JavaScript when your content (image, API data) is ready. The transition from blur(8px) to blur(0) is smooth because the browser interpolates filter values natively.
How does CSS filter affect performance?
Every element with a non-none filter value creates a new stacking context and is promoted to a compositing layer. This means:
- Positive: filter-animated elements do not trigger repaints of surrounding content — they composite independently.
- Negative: each compositing layer consumes GPU memory and texture memory. Too many simultaneously is problematic on lower-end devices.
blur() is particularly expensive because it samples a radius of pixels around each output pixel. A large blur(20px) on a large element is one of the most GPU-intensive CSS operations you can trigger.
/* Promote to GPU layer before the animation starts */
.will-animate-filter {
will-change: filter;
}
/* Remove will-change after animation to free GPU memory */
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
}); backdrop-filter is even more expensive than filter because it must sample and process the rendered output of everything behind the element before compositing. Use it intentionally — one or two glassmorphism panels per page is fine; a modal overlay with backdrop-filter behind a list of cards with their own backdrop-filter is a performance trap.
A concrete rule: measure first. Open DevTools Performance panel, record while hovering/animating, check for dropped frames. If you see compositing taking more than 2ms per frame, reduce blur radius or reduce the number of simultaneous filtered elements.
What is the browser compatibility for CSS filter and backdrop-filter?
filter has near-universal support. All modern browsers have supported it since 2016. No vendor prefixes required.
backdrop-filter requires a prefix for Safari:
-webkit-backdrop-filter: blur(12px); /* Safari 9+ */
backdrop-filter: blur(12px); /* Chrome 76+, Edge 79+, Firefox 103+, Safari 18+ */ Always include both. Modern Safari (from version 18, 2024) supports the unprefixed version, but a significant share of Safari users is on older versions. The -webkit- prefix costs nothing and ensures support back to 2015.
Internet Explorer does not support either property. If IE support is a requirement, apply a graceful fallback via @supports.
Sources: Can I Use — CSS filter, Can I Use — backdrop-filter.
Practical recipes: real-world filter patterns
Image hover: color reveal
.photo { filter: grayscale(100%) brightness(0.9); transition: filter 0.4s ease; }
.photo:hover { filter: none; } Dark mode image inversion
@media (prefers-color-scheme: dark) {
img.invertible { filter: invert(1) hue-rotate(180deg); }
} This inverts the lightness while largely preserving perceived hues — blue sky stays approximately blue. Works best on diagrams, illustrations, and screenshots with white backgrounds.
Colored drop-shadow on SVG icon
.icon {
filter: drop-shadow(0 4px 8px rgba(99, 102, 241, 0.7));
}
/* box-shadow would cast a rectangle, not the icon shape */ Glassmorphism navigation bar
.navbar {
position: sticky;
top: 0;
background: rgba(10, 10, 16, 0.6);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
@supports not (backdrop-filter: blur(1px)) {
.navbar { background: rgba(10, 10, 16, 0.95); }
} The saturate(180%) alongside blur() prevents the blurred background from looking too grey and washed out — a common refinement used by macOS and iOS system UI.
Use the generators to experiment with values and copy production-ready CSS without writing the syntax by hand.