Getting started
Install it, and get one lens on screen.
Liquid Glass Easy brings Apple's Liquid Glass — the material behind iOS 26 and iOS 27 — to Flutter: a shader lens that refracts, magnifies, tints and bends whatever is behind it, and answers touch like a soft body. Not a blur with a white overlay — the content underneath actually bends.
The widget at the centre is LiquidGlassLens: one piece of glass. Put several on a screen and wrap them in a LiquidGlassBatch, so they share one read of the backdrop instead of paying for it each. Everything else on this site is a way to style a lens, a way to make it respond, or a ready-made control with one already wired in.
The glass comes in two weights. The lens refracts through a shader; lite glass is the same look without the shader — frost, tint and rim, no refraction — for pages with many lenses, slower devices, or the web. Flip it here and every demo on this site switches; the same button sits in the header of every page.
Switches every demo on this site between full glass and lite glass. It is the same button as the one in the header.
Install#
dependencies:
liquid_glass_easy: ^4.3.0flutter pub getimport 'package:liquid_glass_easy/liquid_glass_easy.dart';A lens, anywhere#
On Impeller — Flutter's default renderer on modern iOS and Android — a lens needs no setup at all. Drop it over your UI and it refracts whatever your app already painted behind it.
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')),
),
),
),
],
)The lens takes its size from layout, so give it one — a SizedBox, a Positioned, or a child with an intrinsic size. The child is always clipped to the glass shape; add your own Padding to inset it.
More than one lens#
Several lenses are just several LiquidGlassLens widgets. When a screen has many of them — a list of glass cards, a row of chips — wrap that part of the tree in a LiquidGlassBatch. On Impeller every lens would otherwise read the backdrop for itself; inside a batch they share one read, and each still keeps its own shape, style and child.
const cardStyle = LiquidGlassStyle(
shape: LiquidGlassShape.squircle(cornerRadius: 28),
refraction: LiquidGlassRefraction(distortion: 0.1, distortionWidth: 26),
);
LiquidGlassBatch(
child: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: 20,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.only(bottom: 14),
child: SizedBox(
height: 110,
child: LiquidGlassLens(
style: cardStyle,
child: Center(child: Text('Card $i')),
),
),
),
),
)The batch is Impeller's saving. On Skia and the web the view already captures once for every lens inside it, so the same tree runs there unchanged and the batch simply has nothing left to share. There is no limit on how many members a batch holds; the Batch page has the details, and Blend is the other way to put lenses together — when they should fuse into one piece of glass.
The same lens on Skia and the web#
Skia has no live-backdrop shader, so the lens has nothing to read unless you hand it something. Wrap your content in a LiquidGlassView and give it a backgroundWidget — the view captures that background, and every lens inside its child refracts the capture.
LiquidGlassView(
backgroundWidget: const MyBackground(),
child: Center(
child: SizedBox(
width: 300,
height: 160,
child: LiquidGlassLens(
style: const LiquidGlassStyle(
shape: LiquidGlassShape.squircle(cornerRadius: 40),
),
child: const Center(child: Text('refracts the capture')),
),
),
),
)That same tree is safe to ship on Impeller, and it is the reason you only write one. The view detects the engine itself — no flag, no if (Platform...) — and on Impeller it takes no captures at all: the backgroundWidget simply paints where it always would, and the lenses inside child go straight to the Impeller backdrop pipeline and refract the live pixels behind them. The capture path exists only on the engine that has no other option.
Without a view on Skia#
Nothing breaks. A lens with no background to refract degrades to a frosted surface — backdrop blur, tint and rim — and logs a one-time debug notice telling you which lens it was. You get a plausible glass panel instead of an empty rectangle.
Lite glass: the shortcut#
Lite glass is the same material without the shader: frost, tint and a lit rim, but no refraction. Nothing to compile, no capture on Skia, no slot in the lens budget on Impeller. You reach it three ways, and none of them needs a switch to be turned on first.
As a widget. LiquidGlassLite is a surface of its own — a card, a row, a sheet — that was never a lens. Give it a shape, a blur and a colour and it draws lite glass, on any engine, anywhere in the tree.
LiquidGlassLite(
shape: const LiquidGlassShape(cornerRadius: 22, borderWidth: 1.5),
blur: const LiquidGlassBlur(sigmaX: 3, sigmaY: 3),
color: const Color(0x33FFFFFF),
child: child,
)On one lens. Give a lens's style a liteGlass value and that lens draws lite while every other lens on the screen stays full glass.
LiquidGlassLens(
style: const LiquidGlassStyle(liteGlass: LiquidGlassLitePickup.backdrop),
child: child,
)For the whole app. Two static switches on LiquidGlassEngine, one per engine. Turn one on and every lens and every component — the slider, the switch, the buttons, the bars, the sheet — draws lite glass automatically, with no change to any of them. Set it in main() before runApp, so the first lens already builds lite. Both are false by default: leave them out, or set them back, and the app is full glass again.
void main() {
// Every lens and component in the app draws lite glass on this engine.
LiquidGlassEngine.liteGlassOnSkia = true; // Skia and the web
LiquidGlassEngine.liteGlassOnImpeller = true; // Impeller
// Full glass again: set either one back to false (the default).
// LiquidGlassEngine.liteGlassOnSkia = false;
// LiquidGlassEngine.liteGlassOnImpeller = false;
runApp(const MyApp());
}One line in main(), and the shaders are ready#
The glass runs on fragment shaders. Call ensureLoaded before runApp and they are compiled and loaded before the first lens builds — ready the moment it appears.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await LiquidGlassShaders.ensureLoaded();
runApp(const MyApp());
}
Comments
Comments are GitHub Discussions — reply from either place.