Liquid Glass Easy
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.
What's New in 3.2
Sharper, faster blending on both backends
3.2 is a rendering pass focused on the LiquidGlassBlender and the engine split:
- Per-backend shaders. The single-lens shaders now ship a backend-specific
build: Impeller keeps the hardware-derivative gradient, while Skia/web loads a
dedicated entry with an analytic gradient (
dFdxis invalid SkSL). Programs are cached per backend and resolved automatically — no API change needed. - Skia blend quality + blur. The metaball blend on Skia now uses an analytic merged-field gradient and has in-shader blur re-enabled, with chromatic aberration applied before the blur to match Impeller.
- Cheaper blend on Impeller. The engine-blur backdrop pass is now clipped to the tight glass region instead of the whole surface, so the costly pass only covers where the merged glass actually is.
debugClipBounds— a new diagnostic flag onLiquidGlassBlenderthat outlines the backdrop clip region (off by default).
What's New in 3.1
LiquidGlassBlender — 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's New in 3.0
Version 3.0 is a major step toward a simpler, more flexible API.
LiquidGlassLens — a lens you can place anywhere
The headline change. The previous lens API was position-driven: you declared
a LiquidGlassView, gave it a backgroundWidget, and listed lenses with
explicit width / height / position values.
LiquidGlassLens is layout-driven instead. Drop it anywhere in your widget
tree — inside a Row, a ListView, a Stack, a Card — and it is exactly
where layout puts it and exactly as big as its constraints/child make it. No
position, no width/height parameters.
SizedBox(
width: 220,
height: 120,
child: LiquidGlassLens(
style: const LiquidGlassStyle(
shape: LiquidGlassShape.squircle(cornerRadius: 36),
),
child: const Center(child: Text('glass')),
),
)
Works on both Impeller and Skia
LiquidGlassLens automatically resolves the best render 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
LiquidGlassViewto give the lens a background to refract.
LiquidGlassStyle — one styling vocabulary
Every glass surface — the lens, the components, the nav pill — is described with a single reusable descriptor: shape + appearance + refraction.
const LiquidGlassStyle(
shape: LiquidGlassShape.continuousRoundedRectangle(cornerRadius: 24),
appearance: LiquidGlassAppearance(color: Color(0x22FFFFFF)),
refraction: LiquidGlassRefraction(distortion: 0.08, distortionWidth: 28),
);
Drop-in glass components
Ready-made widgets, each built on LiquidGlassLens with tuned defaults. On
Impeller they all refract the live backdrop and work anywhere with no
setup. The difference shows on Skia: some components refract the app
content behind them (so they need an ancestor LiquidGlassView), while others
supply their own background and work anywhere on both engines.
LiquidGlassButton— refracts the content behind it. Anywhere on Impeller; on Skia place it inside aLiquidGlassView(frosted fallback without one).LiquidGlassSlider— jelly thumb that refracts the track as it moves. Self-contained: it owns its background, so it works anywhere on both Impeller and Skia — noLiquidGlassViewneeded.LiquidGlassToggle— refracts its own track. Self-contained, so it works anywhere on both Impeller and Skia — noLiquidGlassViewneeded.LiquidGlassAppBar— refracts the content behind it. Anywhere on Impeller; needs aLiquidGlassViewon Skia.LiquidGlassBottomNavBar— refracts the content behind it. On Skia, use it inside aLiquidGlassScaffold(which provides theLiquidGlassView). To place it anywhere on Impeller, use theLiquidGlassBottomNavBar.withImpeller(...)constructor.LiquidGlassTabBar— refracts the content behind it. Anywhere on Impeller; needs aLiquidGlassViewon Skia.LiquidGlassScaffold— a Scaffold-style layout that owns the glass pipeline (its ownLiquidGlassView), so its child lenses refract the body anywhere on both engines.LiquidGlassDraggable— a drag wrapper for any lens; inherits whatever the lens it wraps requires.LiquidGlassJelly— the squash/stretch physics as a reusable widget; inherits whatever the content it wraps requires.
Why the change?
The old API made you describe a lens as a fixed element, pinned by explicit
width / height / position, inside a LiquidGlassView with a
backgroundWidget. That fought Flutter's layout model — it wasn't
flexible or scalable for Impeller and Skia, and you always had to supply a
background for Impeller, even though the engine can already render the live
backdrop on its own.
LiquidGlassLens flips this around. It is a normal layout widget: it sizes and
positions itself like anything else in the tree, and it's compatible with both
Impeller and Skia. On Impeller it works anywhere — it refracts the live backdrop
with no LiquidGlassView and no background widget at all, though you can still
place it inside a LiquidGlassView if you want to support Impeller and Skia
together: on Impeller it uses the backdrop filter and on Skia it captures the
background, and the lens handles both automatically. On Skia, wrap it in a
LiquidGlassView with a backgroundWidget and it refracts that captured
background.
Migration note: the old position-driven lens API (
LiquidGlass) is no longer used — it has been replaced byLiquidGlassLens. Write new code againstLiquidGlassLensand the drop-in components.
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
- True liquid glass visuals — real-glass look and physics with fluid transparency, soft highlights, and light-bending refraction.
- Lens-anywhere —
LiquidGlassLensis layout-driven; place it anywhere in the tree with no position/size params. - Liquid blending —
LiquidGlassBlenderfuses 2–6 lenses into one metaball surface (not yet optimized for Skia). - Impeller and Skia — auto-resolved render paths: live backdrop on Impeller, captured background on Skia, frosted fallback otherwise.
- Real-time lens 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
ClassicBorderor background-tintedOpticalBorder. - Drop-in components — buttons, sliders, toggles, app/tab/nav bars, scaffold.
- Shader-driven, GPU-accelerated — smooth, high-FPS performance.
- Cross-platform — Android, iOS, Web, macOS, and Windows.
Installation
dependencies:
liquid_glass_easy: ^3.2.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.0–1.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 usingLiquidGlassClipQuality.exact. The squircle has its own shader-matchedClipPath, soexactmakes the clipped child/blur silhouette follow the true L^n curve instead of a plain rounded rectangle.continuousRoundedRectangle: leaveclipQualityat its default (roundedRectangle). A rounded-rectangle clip already hugs the continuous corner so closely that there's effectively no visible difference from theexactcontinuous clipper — that continuous clipper is only there as an experiment, andexactjust adds an extra (more expensive) save layer for no real gain. Only reach forexacthere 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.0–3.0. |
ambientIntensity |
Ambient rim contribution, keeping it visible on the shadow side. 1.0 default. Range 0.0–5.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.
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),
),
],
);
.withImpelleris 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 aLiquidGlassScaffoldwith a realbody.
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.
Recommended Settings (Skia capture)
- General use:
useSync: true,pixelRatio: 0.8–1.0 - Performance-focused:
useSync: false,pixelRatio: 0.5–0.7
For full-screen backgrounds,
pixelRatioof 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!