filter post-processes an element's own pixels. You can chain functions with spaces, not commas (`filter: blur(2px) brightness(1.2)`), applied left to right.
blur() is a Gaussian blur; brightness/contrast/saturate scale pixel values; grayscale/sepia/invert are colour transforms; hue-rotate spins the colour wheel. Every one of them is shorthand for an SVG filter primitive, so for finer control you can write your own SVG filter and reference it with filter: url(#id).
The usual trap is performance — bigger blur radii and bigger elements cost more GPU time, and animating filter on a large image every frame visibly stutters. filter also creates a new stacking context, same as opacity or transform, trapping any child z-index inside it.
In practice most uses touch one value: blur for a loading skeleton, grayscale for a disabled state, hue-rotate to retint an illustration for dark mode.
When to use
Safest when you flip a single value to express state (disabled = grayscale, loading = blur). Avoid animating a large blur on a full-bleed image every frame.