Filter
Applies a graphical effect to an element and everything inside it. Check out the MDN for how to use.
The element is rendered off screen and the effect is applied to the result, so a filter affects the element’s background, borders, text and children as one image rather than each of them separately — as on the web.
.card { filter: grayscale(1); width: 260px; padding: 16px; margin-top: 16px; background-color: white; border: 2px solid black; border-radius: 8px; } .title { font-size: 20px; margin-bottom: 12px; color: #222; } .swatches { display: flex; gap: 8px; } .swatches > view { flex: 1; height: 48px; border-radius: 4px; } .card > button { margin-top: 12px; }
The button inside the card is filtered along with the rest of it and still takes clicks.
Functions
| Function | Range | Notes |
|---|---|---|
blur(<length>) | 0 and up | Reaches about three times the radius past the element’s box |
drop-shadow(<x> <y> [blur] [color]) | Casts from the element’s alpha, not its box | |
brightness(<n>) | 0–2, 1 is none | |
contrast(<n>) | 0–2, 1 is none | |
grayscale(<n>) | 0–1 | |
hue-rotate(<angle>) | -180deg–180deg | |
invert(<n>) | 0–1 | |
opacity(<n>) | 0–1 | Applies to the element as a group, unlike the opacity property |
saturate(<n>) | 0–2, 1 is none | |
sepia(<n>) | 0–1 | |
grain(<n> [phase]) | 0–1 | Not a web function. Adds noise. See phase |
pixelate(<length>) | 0 and up | Not a web function. Quantises to blocks of this size |
posterize(<n>) | 2 and up, 0 is none | Not a web function. Rounds each channel to n evenly spaced levels |
scanlines(<n> [period] [phase]) | 0–1 | Not a web function. Darkens every other band of rows. See phase |
tint(<color>) | white is none | Not a web function. Multiplies the result by a colour |
chromatic-aberration(<length>) | 0 and up | Not a web function. Pulls red and blue apart along x |
Functions combine in one declaration, and are applied in the order listed above rather than the order they are written:
.el {
filter: blur(2px) saturate(1.4) hue-rotate(20deg);
}
drop-shadow() follows the element’s alpha, so it traces text and transparent images rather than the border box that box-shadow follows. It takes an x and y offset, an optional blur radius and an optional colour, in that order:
.el {
filter: drop-shadow(4px 6px 8px rgba(0, 0, 0, 0.4));
}
The colour ops are applied to the element only, so the shadow keeps the colour you gave it under a grayscale() or hue-rotate(). A shadow cast from a blurred element is blurred with it.
Unlike box-shadow this takes no spread and no inset. Both parse — the same parser reads them — and both are ignored.
tint() multiplies, so it can only darken — pair it with brightness() to go the other way. grayscale(1) tint(...) recolours an element wholesale, which is the usual way to grey out a disabled panel or flash one on damage:
.damaged {
filter: brightness(1.4) tint(#f66);
}
Animating grain and scanlines
Neither takes a time input. Both take a trailing phase instead, and a @keyframes animation on it is what makes them move — which keeps a static filter free and puts the per-frame cost only where you asked for it.
@keyframes roll {
from { filter: scanlines(0.3 4px 0px); }
to { filter: scanlines(0.3 4px 4px); }
}
For scanlines the phase is a length, and one full period of it rolls the bands once — the drift a CRT’s hum bar has. For grain it is a plain number fed to the noise hash, so any change gives an unrelated field rather than the same one slid across: that is what makes it flicker rather than crawl.
The timing function decides the rate. linear resamples every frame; steps(12) gives you grain at twelve frames a second, which is closer to how film grain actually reads.
filter is animatable, so it works in transition and @keyframes.
What to know
filter is implemented for the UGUI renderer only. It does nothing under UIToolkit, which has no equivalent.
A filtered element stays interactive: pointer events are mapped back through the image that stands in for it, so buttons and inputs inside one keep working, and an event nothing inside handles — a wheel, a drag — carries on to the ancestors the element still belongs to. The one thing it does not get is real depth against unfiltered content drawn over it: a filtered subtree is placed a hair in front of the image standing in for it, so it wins that overlap whichever way the two are actually stacked.
An ancestor’s overflow: hidden clips the filtered result, as it does on the web — so a blur that spills past a scroll container’s edge is cut off there rather than escaping it, and a point past that edge does not reach the element either. An overflow: hidden on or inside the filtered element still clips normally.
rotate and scale apply to the filtered result, so a rotated card’s blur is rotated with it and a scaled one’s blur grows with it, as on the web.
Very large blur radii cost more than small ones: the radius decides how many blur passes run, up to four.
Most functions are a value the composited image is drawn with, so changing one only redraws that image. blur(), drop-shadow() and chromatic-aberration() are the exceptions — each widens the area that has to be captured, so changing one re-renders the element’s subtree. Aberration only re-renders when its whole-pixel margin changes, so moving one through a few pixels re-renders a few times rather than on every frame.
That accounting is the whole cost: re-applying an element’s style no longer touches its background, rounding or text when none of them moved, so an animation on a colour op holds its capture for as long as the subtree itself is still.