-
Notifications
You must be signed in to change notification settings - Fork 765
Description
CSS blur() and backdrop-filter: blur() are currently uniform across an element. Modern UI frequently needs a progressive (gradient) blur: blur that ramps from 0 → N in a direction (e.g. toolbar/footer scrims, "fade-to-blur" overlays, edge treatments).
This issue proposes a progressive blur function modelled on linear-gradient() syntax. The initial focus is the common progressive (linear) blur use case, but the syntax mirrors gradients to maintain consistency and enable future radial/conic variants.
Primary use cases
- Backdrop blur scrims (common on toolbars / bottom sheets): blur increases toward one edge to improve legibility without fully obscuring content.
- Edge fade effects: progressively blur content toward a boundary (e.g. behind a floating header) rather than uniformly blurring the whole element.
- Design parity with native UI: progressive blur is a standard primitive in native platforms (iOS/macOS
UIVisualEffectView, etc.) and design tools (Figma, Sketch); web currently requires heavy workarounds (layering + masks) that don't actually vary blur radius — they just blend between unblurred and uniformly-blurred output.
Proposed API: introduce a linear-blur() filter function (gradient-like)
Rather than overloading blur(), introduce a dedicated function that borrows the mental model and direction syntax of linear-gradient(). This also leaves a clear path to future radial-blur() / conic-blur() variants.
Proposed syntax:
filter: linear-blur([<angle> | to <side-or-corner>]?, <blur-stop-list>);
backdrop-filter: linear-blur([<angle> | to <side-or-corner>]?, <blur-stop-list>);Where <blur-stop-list> follows the same pattern as gradient colour stops:
<blur-stop-list> = <blur-stop> , <blur-stop>#
<blur-stop> = <length [0,∞]> <length-percentage>?
Each stop defines a blur radius, with an optional position along the gradient axis. Positions default to even distribution when omitted (matching linear-gradient() behaviour). The grammar parses unambiguously: a <length> blur value is always followed by an optional <length-percentage> position.
Examples:
/* Simple: 0px at top, 24px at bottom (default direction) */
.scrim {
backdrop-filter: linear-blur(0px, 24px);
}
/* With explicit direction */
.scrim {
backdrop-filter: linear-blur(to bottom, 0px, 24px);
}
/* With angle */
.scrim {
backdrop-filter: linear-blur(90deg, 0px, 24px);
}
/* With stop positions */
.scrim {
backdrop-filter: linear-blur(to bottom, 0px 0%, 0px 50%, 24px 100%);
}
/* Multiple stops */
.tilt-shift {
filter: linear-blur(to bottom, 8px, 0px 40%, 0px 60%, 8px);
}Semantics
This is spatially-varying blur, not blended output.
The blur radius at each point along the gradient axis is interpolated between the specified stops. This produces true progressive blur — the convolution kernel size varies spatially across the element.
This is distinct from the current workaround of applying a uniform blur and blending it with the original via mask-image:
/* Current workaround — NOT what this proposal does */
.scrim {
backdrop-filter: blur(24px);
mask-image: linear-gradient(transparent, black);
}The workaround blends between unblurred and uniformly-blurred output. linear-blur() instead varies the blur radius itself — preserving detail in low-blur regions rather than hiding uniform blur behind opacity.
Implementation considerations:
True spatially-varying blur is more computationally expensive than uniform blur. This is a known tradeoff for the capability. Implementations may use approximations (mipmap-based approaches, multi-pass algorithms, etc.) provided the visual result is consistent with spatially-varying kernel sizes — preserving detail in low-blur regions rather than blending uniform blur behind opacity.
Additional semantics:
- Stop positions are optional; when omitted, stops are evenly distributed (consistent with
linear-gradient()). - The intent is to follow the existing coordinate space / region model that filter functions already use.
Animation / transitions
linear-blur() should be animatable by interpolating stop values. For example, transitioning between:
backdrop-filter: linear-blur(0px, 24px);
backdrop-filter: linear-blur(8px, 48px);...would interpolate each stop independently. This enables scroll-driven blur ramps and other dynamic effects.
Interpolation follows the same rules as gradient animation — matching stops interpolate directly; mismatched stop counts use the same normalisation behaviour as linear-gradient().
Why a new function instead of overloading blur()?
-
Avoids overloading existing
blur()parsing/semantics. Adding directional and multi-stop parameters toblur()would complicate its grammar and change its mental model from "uniform effect" to "spatially-varying effect." -
Reuses the well-known gradient mental model. Developers already understand
linear-gradient(direction, stop, stop, ...);linear-blur()follows the same pattern. -
Creates a clear family for future variants.
radial-blur()andconic-blur()become natural extensions without needing to redesign the API — mirroring howlinear-gradient(),radial-gradient(), andconic-gradient()form a family.
Out of scope
The following are intentionally out of scope for this proposal, but represent logical next steps or related problem spaces:
Future blur variants (separate proposals):
radial-blur()— blur varies from centre outward (or inward); enables vignette blur, focal point effects. Syntax would mirrorradial-gradient().conic-blur()— blur varies angularly around a point. Syntax would mirrorconic-gradient().
Different problem spaces:
- Motion/velocity-coupled blur — blur based on movement direction/speed; see Motion Blur Effects #11134.
- Mask-driven blur maps — using arbitrary images to control blur radius per-pixel; potentially a more general but more complex feature.
Open questions
- Should this live in Filter Effects (FXTF) as a new filter function (
linear-blur())? - Any known implementation/performance constraints for spatially-varying blur radius beyond those noted above?
- Should stop position syntax exactly match
linear-gradient(), including colour hint equivalents (blur "hints" for easing between stops)?
Related
- Broader "Motion Blur Effects" discussion: Motion Blur Effects #11134
- Alternative approach (generalised filter masking): [filter-effects-2] Allow masking of filters #13288
Changelog
- 5 Jan 2026: Clarified that
linear-blur()produces spatially-varying blur radius (variable kernel), not blended output. Added implementation cost acknowledgement and flexibility note. Added animation/transition behaviour. Expanded design rationale for new function vs extendingblur().