liquid_glass_easy 3.5.0 copy "liquid_glass_easy: ^3.5.0" to clipboard
liquid_glass_easy: ^3.5.0 copied to clipboard

A Flutter package that provides real-time liquid glass lens effects with distortion, magnification, refraction, and blur for stunning UI visuals.

Liquid Glass Easy #

pub version pub likes pub points license

A Flutter package that brings Apple's iOS-style Liquid Glass to your app with real-time, interactive lenses. These dynamic lenses magnify, distort, blur, tint, and refract the content behind them — recreating the iOS 26 Liquid Glass look with stunning, glass-like effects that respond fluidly to movement and touch.

Liquid Glass Blending Liquid Glass Bottom Nav Bar

Liquid Glass Flex — touch deformation

Liquid Glass Slider Liquid Glass Toggle

Liquid Glass Control Center Liquid Glass Notification

LiquidGlass Example


Building Blocks #

Block API What it does
Glass LiquidGlassLens The surface itself. Layout-driven — drop it anywhere and it refracts what's behind it. Styled with LiquidGlassStyle: shape, appearance, refraction.
Touch LiquidGlassTouch How glass answers a finger. Carries LiquidGlassFlex: press and it swells, drag and it deforms, release and it springs back.
Jelly LiquidGlassJelly Squash-and-stretch motion driven by a value, tuned by LiquidGlassJellyConfig — the physics behind the slider thumb and nav pill.
Blend LiquidGlassBlender Merges 2–6 lenses into one surface, joined by a smooth metaball bridge.
View LiquidGlassView The Skia / web background pipeline. Not needed on Impeller.
Components LiquidGlassSlider, LiquidGlassToggle, LiquidGlassButton, LiquidGlassAppBar, LiquidGlassTabBar, LiquidGlassBottomNavBar, LiquidGlassScaffold, LiquidGlassDraggable Ready-made controls, each a lens with the blocks above already wired.

Render paths — Impeller, Skia, and the fallback #

LiquidGlassLens resolves the best path for the engine your app is running on. The widget tree you write is identical in every case:

Engine / setup Behavior
Impeller (Flutter's default on modern iOS/Android) The lens refracts the live backdrop — whatever your app painted behind it. No LiquidGlassView and no background widget needed at all. Just drop the lens over any UI.
Skia with an ancestor LiquidGlassView (+ backgroundWidget) The lens refracts the view's captured background, wherever it sits inside the view's child.
Skia without a view Refraction isn't possible, so the lens gracefully degrades to a frosted look (backdrop blur + tint + border) and logs a one-time debug notice.

In short: on Impeller it just works anywhere; on Skia you wrap your content in a LiquidGlassView to give the lens a background to refract.


Touch — glass that answers a finger #

Pass a touch: and the lens becomes a soft body. Press it and it swells under your finger; drag it and it elongates along the pull, pinches in the cross axis, leans after your thumb, then springs back with a wobble. The lens never moves — only its shape and its content deform.

Liquid Glass Flex — touch deformation

LiquidGlassLens(
  touch: const LiquidGlassTouch(
    flex: LiquidGlassFlex(),
  ),
  style: const LiquidGlassStyle(
    shape: LiquidGlassShape.continuousRoundedRectangle(cornerRadius: 26),
  ),
  child: myContent,
)

touch is a group, not a single effect: it carries the whole response a surface has to a finger, so a control's feel travels as one value the way its whole look travels as a LiquidGlassStyle. Today it holds flex — the deformation — and further members land as new fields, not as a new parameter on every component.

The four edges spring independently, so the half nearest your finger deforms more than the far half — the asymmetry a scale transform cannot produce. grip controls how localized that is (0 = symmetric wherever you touch, 1 = fully local), squeeze takes the along-axis gain back out of the cross axis so an elongated lens genuinely gets thinner, and lean slides the body after the finger. .subtle(), .uniform() and .pronounced() are tuned starting points.

null — the default — adds nothing to the tree: no gesture listener, no ticker, no cost.


Blend — fuse lenses into one liquid surface #

Wrap two to six LiquidGlassLens descendants in a LiquidGlassBlender and their silhouettes merge into a single liquid glass surface: as neighbouring lenses approach they grow a smooth metaball bridge, and they pull apart as you separate them — each member keeps its own corner style through the merge.

LiquidGlassView(
  backgroundWidget: myBackground,
  child: LiquidGlassBlender(
    smoothness: 56,
    style: const LiquidGlassStyle(
      shape: LiquidGlassShape.continuousRoundedRectangle(cornerRadius: 36),
    ),
    child: Stack(
      children: const [
        Positioned(left: 40, top: 80, child: SizedBox(width: 120, height: 120, child: LiquidGlassLens())),
        Positioned(left: 120, top: 110, child: SizedBox(width: 100, height: 100, child: LiquidGlassLens())),
      ],
    ),
  ),
)

It works on both backends — Impeller samples the live backdrop, Skia refracts the captured background (place it inside a LiquidGlassView).

⚠️ A note on blur on Skia. In-shader blur on the Skia capture path may cost performance when the lenses are big or the blur is big. Also, high blur (above ~7) doesn't match the look of a real backdrop blur. It isn't clamped, though — the value is left unrestricted so you can push it if you want; just expect it to diverge from the Impeller look at high sigmas.


What each component needs #

On Impeller every component refracts the live backdrop and works anywhere with no setup. The difference shows on Skia: some refract the app content behind them (so they need an ancestor LiquidGlassView), while others supply their own background and work anywhere on both engines.

Component Skia requirement
LiquidGlassSlider None — self-contained, it owns its background. Works anywhere on both engines.
LiquidGlassToggle None — refracts its own track. Works anywhere on both engines.
LiquidGlassScaffold None — it is the pipeline; its child lenses refract the body on both engines.
LiquidGlassButton Needs an ancestor LiquidGlassView (frosted fallback without one).
LiquidGlassAppBar Needs an ancestor LiquidGlassView.
LiquidGlassTabBar Needs an ancestor LiquidGlassView.
LiquidGlassBottomNavBar Use it inside a LiquidGlassScaffold, which provides the view. For anywhere on Impeller, use LiquidGlassBottomNavBar.withImpeller(...).
LiquidGlassDraggable, LiquidGlassJelly Inherit whatever the lens or content they wrap requires.

Migration note: the old position-driven lens API (LiquidGlass) is no longer used — it has been replaced by LiquidGlassLens. Write new code against LiquidGlassLens and the drop-in components.

Per-release history lives in CHANGELOG.md.


Why Liquid Glass Easy? #

Unlike traditional glassmorphism or static blur, Liquid Glass Easy simulates real glass physics — complete with refraction, distortion, and fluid responsiveness. It bends live content behind the glass in real time, producing immersive, motion-reactive visuals that bring depth and realism to your UI.


Features #

The systems above are what you build with. These are the qualities they all share:

  • True liquid glass visuals — real-glass look and physics with fluid transparency, soft highlights, and light-bending refraction.
  • Real-time rendering — distortion, blur, tint, and refraction react instantly as content moves behind the glass.
  • Custom shapes — circular rounded rectangles, iOS-style squircles, or Apple-style continuous-corner capsules.
  • Two border modes — stylized ClassicBorder or background-tinted OpticalBorder.
  • Shader-driven, GPU-accelerated — smooth, high-FPS performance.
  • Cross-platform — Android, iOS, Web, macOS, and Windows.

Installation #

dependencies:
  liquid_glass_easy: ^3.5.0
flutter pub get

Getting Started #

1. The simplest case — a lens, anywhere (Impeller) #

On Impeller you don't need a LiquidGlassView or a background. Just drop a LiquidGlassLens over your UI:

import 'package:flutter/material.dart';
import 'package:liquid_glass_easy/liquid_glass_easy.dart';

class DemoGlass extends StatelessWidget {
  const DemoGlass({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        fit: StackFit.expand,
        children: [
          Image.asset('assets/bg.jpg', fit: BoxFit.cover),
          Center(
            child: SizedBox(
              width: 260,
              height: 150,
              child: LiquidGlassLens(
                style: const LiquidGlassStyle(
                  shape: LiquidGlassShape.squircle(cornerRadius: 44),
                  refraction: LiquidGlassRefraction(
                    distortion: 0.13,
                    distortionWidth: 34,
                  ),
                ),
                child: const Center(child: Text('Liquid Glass')),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

2. The Skia path — wrap in a LiquidGlassView #

To make refraction work on Skia, give the lens a background to refract by placing it inside a LiquidGlassView.child:

LiquidGlassView(
  backgroundWidget: const MyBackground(), // required on Skia
  child: Center(
    child: SizedBox(
      width: 300,
      height: 160,
      child: LiquidGlassLens(
        style: const LiquidGlassStyle(
          shape: LiquidGlassShape.squircle(cornerRadius: 40),
          refraction: LiquidGlassRefraction(distortion: 0.12, distortionWidth: 30),
        ),
        child: const Center(child: Text('refracts the captured background')),
      ),
    ),
  ),
)

The exact same LiquidGlassLens code refracts the live backdrop on Impeller and the captured backgroundWidget on Skia — no changes required.

Explore interactively #

You can find the demos shown above under the example/ folder.


Core API #

LiquidGlassLens #

LiquidGlassLens({
  LiquidGlassStyle style = const LiquidGlassStyle(),
  bool visibility = true,        // instant show/hide; hidden = no backdrop cost
  bool? useImpellerBackdrop,     // override engine auto-detection
  Widget? child,                 // clipped to the lens shape
})

Size comes from layout — wrap it in a SizedBox (or let its child/constraints size it). The child is always clipped to the full lens shape; add your own Padding to inset it.

LiquidGlassStyle #

LiquidGlassStyle({
  LiquidGlassShape? shape,                // null → default continuous rounded rect
  LiquidGlassAppearance appearance = const LiquidGlassAppearance(),
  LiquidGlassRefraction refraction = const LiquidGlassRefraction(),
})

copyWith(...) and merge(other) are provided for theme/override patterns.

LiquidGlassRefraction

Property Default Description
distortion 0.1 Bending strength of the distortion (0.01.0).
distortionWidth 30 Thickness of the distortion band around the perimeter, in px.
magnification 1.0 Magnification of content seen through the lens (1.0 = none).
chromaticAberration 0.003 Color-channel separation; 0.0 disables it.
refractionMode shapeRefraction shapeRefraction (follows shape contours) or radialRefraction (circular pattern).

LiquidGlassAppearance

Property Default Description
color transparent Base tint of the lens (often semi-transparent).
blur LiquidGlassBlur() Blur applied to content beneath the glass.
saturation 1.0 1.0 = unchanged, 0.0 = grayscale.
enableInnerRadiusTransparent false Whether the inner, non-distorted region is transparent.

LiquidGlassShape

Pick a corner curve via a convenience constructor:

Constructor Corner style
LiquidGlassShape.roundedRectangle(...) Plain circular corners (cheapest).
LiquidGlassShape.squircle(...) L^n squircle — iOS-style continuous curvature.
LiquidGlassShape.continuousRoundedRectangle(...) Apple capsule-style continuous corners (default; collapses to a clean capsule at full radius).

Common parameters: cornerRadius, borderWidth, borderColor, lightColor, lightIntensity, lightDirection, borderType, and clipQuality (roundedRectangle = cheap circular clip, exact = shape-matched ClipPath).

Tip — choosing clipQuality:

  • squircle: it's worth using LiquidGlassClipQuality.exact. The squircle has its own shader-matched ClipPath, so exact makes the clipped child/blur silhouette follow the true L^n curve instead of a plain rounded rectangle.
  • continuousRoundedRectangle: leave clipQuality at its default (roundedRectangle). A rounded-rectangle clip already hugs the continuous corner so closely that there's effectively no visible difference from the exact continuous clipper — that continuous clipper is only there as an experiment, and exact just adds an extra (more expensive) save layer for no real gain. Only reach for exact here if you can actually see the clipped edge not lining up with the refraction.

LiquidGlassView (Skia background provider) #

LiquidGlassView({
  required Widget backgroundWidget,  // refracted by lenses on Skia
  Widget? child,                     // your UI, containing LiquidGlassLens widgets
  double pixelRatio = 1.0,
  bool realTimeCapture = true,
  bool useSync = true,
  bool? useImpellerBackdrop,
  LiquidGlassRefreshRate refreshRate = LiquidGlassRefreshRate.deviceRefreshRate,
})

Border Modes #

Every shape renders its border in one of two styles through borderType.

Mode Description
ClassicBorder Light/shadow colors sweep around the shape based on the angle between the surface normal and the light direction. Clean, stylized, direct color control.
OpticalBorder (default) An Apple-style, SDF-based rim light that emerges as an optical consequence of the glass shape — background-tinted highlights, dual-sided specular reflections, and a lens height profile. The rim color adapts to whatever sits behind the lens.

Optical Border #

LiquidGlassLens(
  style: const LiquidGlassStyle(
    shape: LiquidGlassShape.squircle(
      cornerRadius: 36,
      borderType: OpticalBorder(
        borderSaturation: 1.5,
        ambientIntensity: 1.0,
        borderSolidity: 0.0,
      ),
    ),
  ),
)
Property Description
borderSaturation Saturation of the border color. 0.0 grayscale, 1.0 unchanged (default), >1.0 more vivid. Range 0.03.0.
ambientIntensity Ambient rim contribution, keeping it visible on the shadow side. 1.0 default. Range 0.05.0.
borderSolidity How far lightIntensity can push the rim toward opaque. 0.0 translucent (default) → 1.0 solid.

Classic Border #

LiquidGlassLens(
  style: const LiquidGlassStyle(
    shape: LiquidGlassShape.roundedRectangle(
      lightColor: Color(0xB2FFFFFF),
      borderType: ClassicBorder(
        borderSoftness: 2.5,
        shadowColor: Color(0x1A000000),
      ),
    ),
  ),
)
Property Description
borderSoftness Feathered edge transition. Higher = softer. Defaults to 1.0.
shadowColor Shadow color on the opposite side of the border for depth. Defaults to Color(0x1A000000).

Common Patterns #

Draggable lens #

LiquidGlassDraggable(
  child: SizedBox(
    width: 200,
    height: 200,
    child: LiquidGlassLens(
      style: const LiquidGlassStyle(
        shape: LiquidGlassShape.roundedRectangle(cornerRadius: 100),
        refraction: LiquidGlassRefraction(distortion: 0.2, magnification: 1.1),
      ),
      child: const Center(child: Text('drag me')),
    ),
  ),
)

Show / hide #

visibility: false disables the glass instantly (no backdrop cost) and removes the child, leaving nothing behind. Wrap the lens yourself to animate the transition:

LiquidGlassLens(visibility: _visible, style: myStyle, child: content)

Lenses inside scrollables #

Not recommended. Liquid glass is designed to float above your content — a fixed lens (a bottom bar, a floating panel, a control overlay) that refracts the scrolling content passing behind it. Putting the lens inside the scrollable, so it scrolls with the list, fights that concept and runs into the overscroll issue below. Prefer a floating lens layered over the list (e.g. in a Stack) instead of a lens placed as a list item.

If you do need a lens inside a scrollable in Impeller, you must disable the overscroll indicator — see below.

Using Lenses inside scrollables (Impeller) #

Android's stretch overscroll isolates the scrollable into its own layer, which can make backdrop lenses render black at the scroll edges. Disable the overscroll indicator for scrollables that contain lenses:

ScrollConfiguration(
  behavior: const MaterialScrollBehavior().copyWith(overscroll: false),
  child: ListView(children: [ /* ...LiquidGlassLens... */ ]),
)

Drop-in Components #

// A glass slider with a jelly thumb that refracts the track.
LiquidGlassSlider(
  value: volume,
  onChanged: (v) => setState(() => volume = v),
);

// A glass toggle.
LiquidGlassToggle(
  value: wifi,
  activeColor: const Color(0xFF0A84FF),
  onChanged: (v) => setState(() => wifi = v),
);

Each component is self-contained and styled through the same LiquidGlassStyle vocabulary. Other components: LiquidGlassButton, LiquidGlassAppBar, LiquidGlassBottomNavBar, LiquidGlassTabBar, LiquidGlassScaffold, LiquidGlassJelly.

Custom icons — SVG, PNG, anything #

Tabs aren't limited to IconData. LiquidGlassTabBarItem.custom draws its glyph through a builder, so any widget works — an SvgPicture, an Image, a CustomPaint:

LiquidGlassTabBarItem.custom(
  label: 'Home',
  iconBuilder: (context, i) => SvgPicture.asset(
    i.selected ? 'assets/home_fill.svg' : 'assets/home.svg',
    width: i.size,
    height: i.size,
    colorFilter: ColorFilter.mode(i.color, BlendMode.srcIn),
  ),
);

The builder is handed the color the bar already resolved for the layer it is drawing, the glyph box size, and whether that layer is the selected one. Tint with i.color and your artwork follows the selected / unselected palette and the morph pill's reveal — the glass-pill bar draws each tab twice per frame, once inside the pill and once outside it, and calls the builder for each. Multi-colour art can simply ignore the colour. LiquidGlassButton and LiquidGlassTabBarAction take a child for the same reason.

Bottom nav bar — standalone with .withImpeller #

LiquidGlassBottomNavBar shows its animated, glass-refracting morph selection pill when it's driven by a LiquidGlassScaffold, which owns the capture pipeline and hands the bar the page as its background.

To use the bar on its own — no LiquidGlassScaffold and no body to pass — use the .withImpeller constructor. On Impeller the bar and its morph pill sample the live backdrop, so just drop it as the last child of a Stack over your page:

Stack(
  children: [
    MyPage(),
    LiquidGlassBottomNavBar.withImpeller(
      items: items,
      selectedIndex: index,
      onChanged: (i) => setState(() => index = i),
    ),
  ],
);

.withImpeller is Impeller-first: on Skia (no live-backdrop shader) it falls back to a plain frosted bar that still shows the content behind it. For the refracting morph pill on Skia, use a LiquidGlassScaffold with a real body.


Snapshot vs Realtime (Skia capture) #

When you use a LiquidGlassView on Skia, choose how its background is captured:

Mode When to Use Config
Realtime Moving backgrounds (scrolling, video) realTimeCapture: true
Snapshot Static backgrounds realTimeCapture: false + viewController.captureOnce()
final viewController = LiquidGlassViewController();

LiquidGlassView(
  controller: viewController,
  backgroundWidget: const MyBackground(),
  realTimeCapture: false,
  child: const MyGlassUI(),
);

// Refresh manually after the background changes:
await viewController.captureOnce();

On Impeller the lens reads the live backdrop directly, so capture settings don't apply — these are a Skia concern.


  • General use: useSync: true, pixelRatio: 0.8–1.0
  • Performance-focused: useSync: false, pixelRatio: 0.5–0.7

For full-screen backgrounds, pixelRatio of 0.5–1.0 balances performance and detail. Smaller regions can afford higher ratios for sharper glass. The final choice depends on the device.


License #

MIT License


Developed by #

Ahmed Gamil

Feel free to open issues or contribute to the project!

231
likes
160
points
7.46k
downloads
screenshot

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter package that provides real-time liquid glass lens effects with distortion, magnification, refraction, and blur for stunning UI visuals.

Repository (GitHub)
View/report issues

Topics

#liquid-glass #glass #shaders #effects #flutter

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on liquid_glass_easy