Skip to content
Blog

Glassmorphism in Flutter: BackdropFilter, Blur, and ImageFiltered

Frosted glass, blurred dialogs, and soft scrims all run through BackdropFilter — one of the most expensive widgets in Flutter. How it works, why it needs a clip, and how to make multiple blurs cheap.

Published on • October 14, 2026

AI Assistant

The frosted-glass header, the blur behind a modal, the scrim that dims a photo under a bottom sheet: in Flutter all of them come from one widget, BackdropFilter. It is also one of the few widgets where the API documentation opens with “this effect is relatively expensive.”

Understanding exactly what it blurs, what bounds the blur to, and when you should not use it at all is the difference between a smooth glass panel and a janky screen.

What BackdropFilter actually does

BackdropFilter applies an ImageFilter to the content that has already been painted beneath it, then paints its own child on top:

ClipRect(
  child: BackdropFilter(
    filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
    child: Container(
      color: Colors.white.withValues(alpha: 0.35),
      child: const Text('Frosted panel'),
    ),
  ),
)

Two things follow from that definition:

  1. The child is not blurred. The text stays crisp; the world behind it does not. If you want the child blurred too, that is ImageFiltered — a different widget entirely.
  2. The blur is bounded by the nearest ancestor clip, not by the child. Without a clip, the filter applies to the entire screen. The ClipRect above is not decoration; it is what limits the blur to the panel’s own rectangle.

That second point is the source of most “my blur covers everything” bugs. The canonical shape is clip first, filter second, child third.

Blur strength: sigma is not a pixel radius

sigmaX and sigmaY are Gaussian standard deviations. A sigma of 4–8 is a believable frosted panel; 16–40 is an abstract soft field. Values look different per platform because the underlying rasterizer approximates the kernel — the same sigma on Android (Impeller) and iOS can differ slightly, so tune against both.

The blendMode parameter controls how the filtered background blends back in. BlendMode.srcOver is the only value supported on every platform and is right for nearly every scene. The docs call out one exception: when a parent uses a temporary buffer — a saveLayer, which Opacity triggers — BlendMode.src can produce more pleasing results. If a blur inside an Opacity widget looks washed out or oddly doubled, that is the knob.

The performance reality

A blur reads the framebuffer behind the widget, convolves it, and writes it back. Non-local filters like blur touch many pixels, so cost scales with area, not complexity. A 60×60 chip is nearly free; a full-screen blur rebuilt every frame during a scroll is not.

Practical rules:

  • Prefer ImageFiltered when you are blurring one widget. The docs are explicit: applying a blur to a single sibling through BackdropFilter is “unnecessarily expensive,” while ImageFiltered does the same job with dramatically better performance. If there is no content behind the thing you are blurring, you do not need a backdrop filter at all.

    // Blurs only the image — cheap
    ImageFiltered(
      imageFilter: ui.ImageFilter.blur(sigmaX: 6, sigmaY: 6),
      child: Image.asset('hero.png'),
    )
  • Clip to exactly what you blur. A ClipRect that matches the child keeps the filtered region small. A ClipRRect additionally rounds the blur to your card shape.

  • Do not rebuild the filter every frame. Hoist ui.ImageFilter.blur(...) into a const or a field. Creating a new filter object per build during an animation forces the render object to update every frame.

  • Animate opacity, not sigma. To fade a blur in, animate the child’s color alpha (or an AnimatedOpacity overlay) rather than interpolating sigma — a changing sigma re-runs the full convolution each frame.

  • Be careful under Opacity and RepaintBoundary. Opacity’s save-layer behavior interacts with blendMode, and an uncached backdrop region repaints whenever anything behind it moves.

One blur for many widgets: BackdropGroup

A list of frosted rows — a notification shade, a translucent settings list — normally means one expensive backdrop blur per row. Flutter added a way to batch them: when several BackdropFilters share a BackdropKey, the engine performs the filtering once and reuses the result.

BackdropGroup(
  child: ListView.builder(
    itemCount: 60,
    itemBuilder: (context, index) {
      return ClipRect(
        child: BackdropFilter.grouped(
          filter: ui.ImageFilter.blur(sigmaX: 40, sigmaY: 40),
          child: Container(
            color: Colors.black.withValues(alpha: 0.2),
            height: 200,
            child: const Text('Blur item'),
          ),
        ),
      );
    },
  ),
)

BackdropFilter.grouped looks up the nearest BackdropGroup and joins it; you can also pass a backdropGroupKey manually. The visual result is identical to N independent blurs while the engine runs one — for a scrolling list of translucent rows, this is the difference between 60fps and something you can feel.

The constraint: overlapping filters must not share a key. If two blurred regions overlap while sharing a BackdropKey, the overlap looks as though only one filter was applied. Group by non-overlapping sets.

Recipes

Frosted app bar

SliverAppBar(
  pinned: true,
  expandedHeight: 200,
  flexibleSpace: ClipRect(
    child: BackdropFilter(
      filter: ui.ImageFilter.blur(sigmaX: 20, sigmaY: 20),
      child: Container(
        color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.6),
        alignment: Alignment.bottomLeft,
        padding: const EdgeInsets.all(16),
        child: const Text('Gallery'),
      ),
    ),
  ),
)

Blurred dialog scrim — put the Stack’s background in, then a Positioned.fill containing the clip + filter + a ColoredBox for the dim, then the dialog. The dialog itself is not blurred; the world behind it is.

Blur-on-hover card — blur the image inside the card with ImageFiltered when hovered, not the backdrop. Hover transitions then animate only that subtree.

Gotchas

  • Blur over platform views (web views, maps, video) has restrictions. On iOS a PlatformView cannot always be blurred; Flutter documents the limitation and the supported workaround for the platform-view backdrop blur case.
  • BackdropFilter inside an unclipped Stack blurs the whole screen. Always clip.
  • Semantics and text stay sharp — the filter never touches the child, so accessibility and text selection are unaffected.
  • Test on a low-end device. Blur cost is GPU-bound and the gap between a flagship and a budget phone is much larger here than for ordinary layout.
  • Consider a gradient instead. Many “blurred” scrims are really just translucent gradients; a ShaderMask or DecoratedBox is an order of magnitude cheaper and often indistinguishable.

Checklist

  • Clip first: ClipRect (or ClipRRect) wraps the BackdropFilter.
  • Use ImageFiltered when blurring a single widget rather than the backdrop.
  • Hoist the ImageFilter out of build; animate alpha, not sigma.
  • Batch repeated blurs with BackdropGroup + BackdropFilter.grouped, but never share a key between overlapping regions.
  • Keep blendMode: BlendMode.srcOver unless you have measured a reason not to.

Sources: BackdropFilter API, ImageFiltered API, Flutter documentation.