haze

Progressive blur for Flutter: a Gaussian over the backdrop, strongest at one edge and fading continuously to nothing, with an optional tint on the same falloff. iOS 26's scroll edge effect as a plain widget.

Stack(children: [
  Image.network(url, fit: BoxFit.cover),
  Positioned(
    left: 0, right: 0, bottom: 0, height: 160,
    child: Haze(
      edge: HazeEdge.bottom,
      sigma: 12,
      tint: CupertinoColors.black.withValues(alpha: .5),
      child: Padding(
        padding: EdgeInsets.all(20),
        child: Text('Light on the dunes'),
      ),
    ),
  ),
]);

Parameters

edge HazeEdge.top / bottom / left / right: where the blur is strongest.
sigma Peak blur at edge, logical px.
tint Wash colour; its alpha is the peak opacity at edge. null = blur only.
enabled Paint the child only, layout unchanged.
child Drawn above the effect. Sizes the widget when given.

The height is whatever the parent gives it. Everything else is worked out.

How it works

Built like Apple's effect (ScrollEdgeEffectView feeding Core Animation's variableBlur a mask) and like Glur: one peak sigma, and a per-pixel fraction of it that reaches zero inside the rectangle. A bundled fragment shader runs it in two separable passes (ui.ImageFilter.shader).

  • Hold, then fade. Blur holds at full strength over the first 41% of the span and the tint over 35%, then both fall on a smootherstep. Both values are measured off the system effect.
  • Geometric sigma ramp. Apparent blur tracks the ratio between sigmas (1 → 3px is huge, 15 → 30px barely visible), so each step along the fade multiplies sigma by the same factor, from 1 physical px up to sigma. Every stretch adds the same perceived blur, with no jump to full strength.
  • No lines. The kernel is tapered to exactly zero at its reach, so a tap entering it as sigma changes row to row adds nothing abruptly. The output is dithered below one 8-bit code.
  • Fits its rectangle. If the fade is too short to carry sigma out smoothly (it must be wider than the blur's own reach), sigma is lowered. Haze.fitSigma(span, sigma) returns the value painted.
  • Premultiplied-correct tint, so it also covers transparent overlay areas, such as the region over a platform view.
  • Bounds are recomputed at paint time. During a route transition the blur is off and eases back in when the page lands.

Without Impeller shader filters, or before the shader loads, it falls back to contiguous bands of plain BackdropFilter blur on the same curve. Those do show their steps. Changing the shader needs a full restart; hot reload does not rebuild it.

Platform views

A shader filter cannot reach a native view: the iOS embedder only forwards uniform ImageFilter.blur to platform views. Native content under the effect has to be brought into Flutter first, for example as a snapshot drawn under it.

Example

cd example && flutter run

Libraries

haze
Progressive (gradient) blur with an optional tint, as one widget.