Skip to content

[filter-effects-2] Progressive blur via linear-blur() #13285

@damian-dp

Description

@damian-dp

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

  1. Backdrop blur scrims (common on toolbars / bottom sheets): blur increases toward one edge to improve legibility without fully obscuring content.
  2. Edge fade effects: progressively blur content toward a boundary (e.g. behind a floating header) rather than uniformly blurring the whole element.
  3. 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.

Image 1 Image 2

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()?

  1. Avoids overloading existing blur() parsing/semantics. Adding directional and multi-stop parameters to blur() would complicate its grammar and change its mental model from "uniform effect" to "spatially-varying effect."

  2. Reuses the well-known gradient mental model. Developers already understand linear-gradient(direction, stop, stop, ...); linear-blur() follows the same pattern.

  3. Creates a clear family for future variants. radial-blur() and conic-blur() become natural extensions without needing to redesign the API — mirroring how linear-gradient(), radial-gradient(), and conic-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 mirror radial-gradient().
  • conic-blur() — blur varies angularly around a point. Syntax would mirror conic-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

  1. Should this live in Filter Effects (FXTF) as a new filter function (linear-blur())?
  2. Any known implementation/performance constraints for spatially-varying blur radius beyond those noted above?
  3. Should stop position syntax exactly match linear-gradient(), including colour hint equivalents (blur "hints" for easing between stops)?

Related


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 extending blur().

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions