flutter_sea_glass 0.1.0-dev.1
flutter_sea_glass: ^0.1.0-dev.1 copied to clipboard
Sea glass and liquid glass for Flutter: tumbled, frosted and colored glass, milky pastel slabs and living shapes, rendered with Impeller.
flutter_sea_glass #
Sea glass and liquid glass for Flutter.
- Sea glass: tumbled, frosted, colored glass that filters the backdrop
deepest at its center, with a fine grain and a frosted rim. Colored glass
can be made clear (
SeaGlassFinish.colorClarity) so it shows and bends what is beneath while white glass stays frosted. - Slab sea glass: thick, milky pastel slabs that glow on the side away
from the light, with terrazzo flecks suspended inside, hand-cut outlines
and soft cast shadows under a scene light (
GlassLight). - Living glass: organic outlines (
LiquidBlob) that flow and breathe, and drops that merge and pinch apart. - Liquid glass: iOS 27-style refraction through a rounded bevel, frost, tint, a directional glint and shapes that melt into each other, with presets fitted against captures of Apple's glass.
It renders everything with Flutter's own pipeline (Impeller and Flutter GPU); it does not use any platform's private material APIs.
| Sea glass | Slab sea glass |
|---|---|
![]() |
![]() |
Based on liquid_glass_renderer. flutter_sea_glass is a fork of liquid_glass_renderer by Tim Lehmann (whynotmake.it), used under the Apache License 2.0 (see LICENSE and NOTICE). It is not affiliated with or endorsed by whynotmake.it, Apple or Google. The widget API keeps the original's names (
LiquidGlass,LiquidGlassLayer, ...), so code written for it moves over by changing the import.This is a prerelease. Full glass needs Impeller and the Flutter GPU API, which is still in preview; APIs and rendering may change. Profile your real screens on physical devices before shipping.
Contents #
- Requirements
- Quick start
- Choose a glass
- Build a UI with sea glass
- The widgets
- Blending shapes
- iOS 27 presets and the Liquid Glass slider
- Tint and color
- Sea glass
- Clear colored glass
- Settings reference
- Refraction
- Glint
- Visibility
- FakeGlass
- Glass on glass and
LiquidGlassCapture - Performance
- Example playground
- The showcase app
- Contributors
Requirements #
- Flutter 3.47 or newer.
- Impeller and Flutter GPU for full glass. Pass
--enable-flutter-gputoflutter run, or turn it on in the app:FLTEnableFlutterGPUinInfo.plist(iOS, macOS) and theio.flutter.embedding.android.EnableFlutterGPUmeta-data inAndroidManifest.xml. The example app shows both. - iOS, Android and macOS are the platforms the renderer is tested on. Web builds compile and render FakeGlass.
Wherever full glass is unavailable, layers switch to FakeGlass
automatically.
flutter pub add flutter_sea_glass
Quick start #
Glass samples the pixels behind it, so put your content and the glass in a
Stack:
import 'package:flutter/widgets.dart';
import 'package:flutter_sea_glass/flutter_sea_glass.dart';
class GlassPill extends StatelessWidget {
const GlassPill({super.key});
@override
Widget build(BuildContext context) {
final brightness = MediaQuery.platformBrightnessOf(context);
return Stack(
children: [
const Positioned.fill(child: MyContent()),
Center(
child: LiquidGlassLayer(
settings: LiquidGlassSettings.ios27Toolbar(brightness: brightness),
child: const LiquidGlass(
shape: LiquidRoundedSuperellipse(borderRadius: 28),
child: SizedBox(width: 220, height: 56),
),
),
),
],
);
}
}
Shaders and the GPU pipeline load on first use, so the first glass on screen
paints FakeGlass for a frame or two. Warm them up before runApp:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await LiquidGlass.precache();
runApp(const MyApp());
}
On Android the GPU part finishes after the first frame.
Choose a glass #
Every look is a LiquidGlassSettings on a LiquidGlassLayer plus a body
color per shape (LiquidGlassAppearance). Pick the settings for the look,
then tune them (see Settings reference).
| Glass | Looks like | Settings | Color a shape with |
|---|---|---|---|
| Sea glass | Tumbled, frosted, colored glass; deepest at its center, a frosted rim, a fine grain | LiquidGlassSettings.seaGlass(); for flat tiles and bars add refractionHeight: 36, refractionAmount: 16 and SeaGlassFinish(gloss: 0), since the preset's 100 pt glossy dome suits pebbles |
LiquidGlassAppearance.seaGlass(color: SeaGlassColors.teal) |
| Clear sea glass | The same, with colored glass clear enough to show and bend what is beneath | LiquidGlassSettings.seaGlass(finish: const SeaGlassFinish(colorClarity: .6)) |
as above |
| Slab sea glass | Thick, milky pastel slabs that glow on the side away from the light and cast soft shadows | LiquidGlassSettings.slabSeaGlass() |
SlabSeaGlass.appearance(SlabSeaGlass.original.primary) |
| Clear slab | The pastel slabs made translucent, frosted or clear | slabSeaGlass with a low scatter and a wider lens (see Recipes) |
as above |
| Liquid glass | iOS 27-style clear glass with a bevel, glint and border | LiquidGlassSettings.ios27Toolbar(brightness: ...) |
LiquidGlassAppearance(tint: ...) |
LiquidGlassLayer(
settings: LiquidGlassSettings.seaGlass(),
// Shapes without an appearance of their own: frosted white sea glass.
defaultAppearance: LiquidGlassAppearance.seaGlass(
color: SeaGlassColors.white.withValues(alpha: .35),
),
child: const LiquidGlass(
shape: LiquidRoundedSuperellipse(borderRadius: 32),
appearance: LiquidGlassAppearance.seaGlass(color: SeaGlassColors.teal),
child: SizedBox(width: 200, height: 64),
),
)
Build a UI with sea glass #
A glass UI is ordinary Flutter layout with glass shapes in it. These steps
build a quick settings panel: toggle tiles that merge like drops, a slider
and a switch between three glasses. The complete app is
example/lib/guide_main.dart
(flutter run -t lib/guide_main.dart).
1. Put content behind the glass. Glass shows and bends what is below
it, so the screen is a Stack with the content (an image, a gradient, a
list) under the glass.
2. Wrap a region in one LiquidGlassLayer. A layer samples the backdrop
once for every shape inside it; that sample is the expensive part. Use one
layer per panel or bar, not one per button. A layer renders at most 16
shapes.
LiquidGlassLayer(
settings: LiquidGlassSettings.seaGlass(),
defaultAppearance: LiquidGlassAppearance.seaGlass(
color: SeaGlassColors.white.withValues(alpha: .35),
),
child: Column(children: [slider, tiles]),
)
3. Make each control a LiquidGlass. The shape is the glass; the child
is the content painted on top of it (icons, labels). Shapes are
LiquidRoundedSuperellipse (squircles and capsules), LiquidOval,
LiquidRoundedRectangle and LiquidBlob (organic outlines).
4. Show state with color. A switched-on tile is colored glass, a
switched-off one frosted white: change its appearance, and the glass
animates nothing it doesn't need to.
GestureDetector(
onTap: onTap,
child: LiquidStretch.seaGlass( // squash and stretch while pressed
child: LiquidGlass.grouped(
shape: const LiquidRoundedSuperellipse(borderRadius: 32),
appearance: LiquidGlassAppearance.seaGlass(
color: on ? SeaGlassColors.teal : SeaGlassColors.white.withValues(alpha: .35),
),
child: SizedBox(width: 168, height: 64, child: Center(child: Text(label))),
),
),
)
5. Merge neighbors with a LiquidGlassBlendGroup. Shapes made with
LiquidGlass.grouped inside a group melt into one surface where they come
within blend logical pixels. Keep blend a little below the gap between
them and they stay apart at rest, then flow together when one is dragged
toward another.
LiquidGlassBlendGroup(
blend: 10, // tiles 12 px apart touch only when pulled together
child: Wrap(spacing: 12, runSpacing: 12, children: tiles),
)
6. Compose controls from several shapes. A slider is two shapes in a
Stack: colored glass for the level and frosted glass for the rest, resized
as the finger moves. A tab bar is a row of grouped pebbles in a blend group,
the selected one wider and colored.
7. Theme it from one place. Keep the settings and colors in one object and rebuild with new ones to switch the whole UI between glasses:
LiquidGlassSettings settingsFor(Glass glass) => switch (glass) {
// A flat lens for tiles: the preset's 100 pt dome suits pebbles.
Glass.sea => LiquidGlassSettings.seaGlass(
finish: const SeaGlassFinish(gloss: 0),
refractionHeight: 36,
refractionAmount: 16,
),
Glass.slab => LiquidGlassSettings.slabSeaGlass(),
Glass.clearSlab => LiquidGlassSettings.slabSeaGlass(
finish: const SeaGlassFinish.slab().copyWith(scatter: .24, colorClarity: .6),
frost: 6,
).copyWith(refractionHeight: 32, refractionAmount: 26, refractionFitsShape: true),
};
8. Warm up and profile. Call await LiquidGlass.precache() before
runApp, and profile on a real device. Glass inside a scrolling list costs
a layer per item; prefer glass that floats over the content.
The widgets #
| Widget | What it does |
|---|---|
LiquidGlassLayer |
Samples the backdrop once and renders every glass shape below it. Owns the shared LiquidGlassSettings. |
LiquidGlass |
One glass shape. Its child paints on top of the glass, clipped to the shape. |
LiquidGlassBlendGroup |
Melts the LiquidGlass.grouped shapes inside it into one surface. |
LiquidGlassCapture |
Captures the backdrop once for glass that sits on other glass. |
LiquidGlassVisibility |
Fades the glass in a subtree in and out. |
FakeGlass |
The fallback without refraction, used automatically where full glass is unavailable. |
LiquidGlassSettings |
Refraction, frost, glint, border and inner shadow, shared by a layer. |
LiquidGlassAppearance |
Tint, color model and visibility, per shape or as a layer default. |
LiquidGlass has four constructors:
| Constructor | Use it when |
|---|---|
LiquidGlass(...) |
A LiquidGlassLayer is above it. |
LiquidGlass.grouped(...) |
It sits in a LiquidGlassBlendGroup and should blend with its neighbors. |
LiquidGlass.auto(...) |
Reusable code that may or may not have a layer above it. It uses a parent layer when there is one and creates its own otherwise. |
LiquidGlass.withOwnLayer(...) |
It needs its own backdrop sample or its own settings, for example glass on glass. |
Put sibling shapes in one layer. Every independent layer samples the backdrop again, and that sample is the expensive part (see Performance).
Shapes are LiquidRoundedSuperellipse (squircles and capsules), LiquidOval
and LiquidRoundedRectangle, each with one corner radius, and LiquidBlob,
an organic outline (see Pebbles and living glass).
Exterior shadows go
on the shape, and the renderer cuts them out behind the translucent glass:
LiquidGlass(
shape: const LiquidRoundedSuperellipse(borderRadius: 28),
shadows: const [
BoxShadow(color: Color(0x24000000), offset: Offset(0, 6), blurRadius: 20),
],
child: const SizedBox(width: 220, height: 56),
)
GlassGlow adds a touch glow inside the glass, and LiquidStretch gives it
the squash and stretch of iOS controls while it is dragged.
Blending shapes #
| Light | Dark |
|---|---|
![]() |
![]() |
Shapes in a LiquidGlassBlendGroup join like drops of water once they come
within blend logical pixels of each other (default 20). Straight edges that
touch stay straight; only the gap between them rounds, as in iOS 27's
GlassEffectContainer.
LiquidGlassLayer(
child: LiquidGlassBlendGroup(
blend: 24,
child: Row(
children: const [
LiquidGlass.grouped(
shape: LiquidOval(),
child: SizedBox.square(dimension: 56),
),
SizedBox(width: 8),
LiquidGlass.grouped(
shape: LiquidRoundedSuperellipse(borderRadius: 28),
child: SizedBox(width: 180, height: 56),
),
],
),
),
)
A layer renders at most 16 shapes; in debug builds it warns when it has to
leave some out. Plain LiquidGlass shapes in the same layer don't blend.
FakeGlass blends a group too (see FakeGlass).
Each shape joins only the members within two blend radii of the nearest one, so the union doesn't depend on shapes far away. When a shape moves, the layer copies its last geometry and redraws only around that shape: dragging one tile of a 16-shape panel redraws about a fifth of the geometry. The result is the same as redrawing all of it.
iOS 27 presets and the Liquid Glass slider #
Glass is configured in two parts. LiquidGlassSettings on the layer holds the
optics and lighting. LiquidGlassAppearance holds the color, either as the
layer's defaultAppearance or per shape.
| Material | Settings | Appearance |
|---|---|---|
Regular (.regular, .glass buttons) |
LiquidGlassSettings(frost: LiquidGlassSettings.ios27RegularFrost(t), tintAmount: t) |
ios27Regular(brightness:) |
| Toolbar | ios27Toolbar(brightness:) |
ios27Toolbar(brightness:) |
Clear (.clear) |
ios27Clear() |
ios27Clear() |
Light and dark variants exist as separate constructors too
(ios27ToolbarLight, ios27ToolbarDark, ...). A layer without a
defaultAppearance uses the toolbar appearance for the platform brightness.
final brightness = MediaQuery.platformBrightnessOf(context);
LiquidGlassLayer(
settings: LiquidGlassSettings.ios27Toolbar(brightness: brightness),
defaultAppearance: LiquidGlassAppearance.ios27Regular(brightness: brightness),
child: const MyToolbar(),
)
t is the slider position described below. The presets are fitted to
Apple's glass at toolbar and button size. They are a
starting point: Apple also varies its glass with the control's role, size and
accessibility settings.
tintAmount: the Liquid Glass slider #
iOS 27 has a Liquid Glass slider in Settings, from Clear (0) to Tinted
(1). Pass its position as tintAmount. The renderer does not read the
system setting; your app decides.
LiquidGlassSettings.ios27Toolbar(brightness: brightness, tintAmount: 0.5)
The slider changes three things, each along a curve fitted to Apple's glass:
- Wash. The iOS 27 color model makes its neutral wash more opaque, and dark glass denser.
- Border. The dark border gets stronger.
- Blur. The presets derive
frostfrom the slider.LiquidGlassSettings.ios27RegularFrost(tintAmount)gives 2, 6.1 and 16.6 pt at 0, 0.5 and 1;ios27ClearFrost(tintAmount)gives 0.35, 1.28 and 16.4 pt, growing at one rate up to the middle tick and at twice that rate beyond it. Passfrostto override.
The glint doesn't change. The direct color model ignores tintAmount, and
tintAmount never changes an explicit frost.
Tint and color #
| Light | Dark |
|---|---|
![]() |
![]() |
Tint a single shape through its appearance. Neighbors in a blend group cross-fade their colors where they meet, from the same backdrop sample:
LiquidGlass.grouped(
appearance: const LiquidGlassAppearance.ios27ToolbarLight(
tint: Color(0xFF0A84FF),
),
shape: const LiquidOval(),
child: const SizedBox.square(dimension: 72),
)
The iOS 27 presets use LiquidGlassColorModel.ios27. It turns one tint color
and its opacity into tones that depend on the brightness of the backdrop,
like Apple's single-tint API. Dark regular glass also gets denser with size:
up to 75 pt on the short side it transmits like light glass, and from 105 pt
it settles at Apple's denser dark material. The smallest shape in a layer
decides. Clear glass (LiquidGlassColorModel.ios27Clear) is the same in light
and dark.
For full control, use the direct model and tune each transfer yourself:
const LiquidGlassAppearance(
colorModel: LiquidGlassColorModel.direct(),
tint: Color(0x663B82F6),
saturation: 1.4,
transmissionGamma: 0.9,
vibrancy: 0.15,
)
A layer where every shape has the same appearance keeps a smaller shader path.
Sea glass #
| Light | Dark |
|---|---|
![]() |
![]() |
Sea glass is glass the sea has tumbled into a frosted pebble. It stays translucent, but its surface is pitted into a fine sugary frost, its rounded rim is paler than its body, and the glass itself is colored throughout, so it is deepest in color where it is thickest. Compared to iOS 27 glass it is a dome rather than a flat face with a bevel: the whole face bends the backdrop like a lens, there is no inner shadow, and the light glints off its rounded shoulders.
Start from the seaGlass preset and give each shape a body color:
LiquidGlassLayer(
settings: LiquidGlassSettings.seaGlass(),
defaultAppearance: LiquidGlassAppearance.seaGlass(
color: SeaGlassColors.white.withValues(alpha: 0.55),
),
child: Row(
children: const [
LiquidGlass(
appearance: LiquidGlassAppearance.seaGlass(color: SeaGlassColors.teal),
shape: LiquidRoundedSuperellipse(borderRadius: 32),
child: SizedBox(width: 170, height: 64),
),
SizedBox(width: 12),
LiquidGlass(
shape: LiquidRoundedSuperellipse(borderRadius: 32),
child: SizedBox(width: 170, height: 64),
),
],
),
)
Under a sea glass finish the appearance tint is the body color, not a wash:
it filters the backdrop like colored glass. The color is what the glass shows
over white at full thickness, and its alpha is the density, so 0 is
colorless frosted glass. SeaGlassColors holds colors sampled from real sea
glass: seafoam, kellyGreen, aqua, cornflower, cobalt, lavender,
citron, amber, brown (root beer), teal, white, ruby, clear and
mist. Each shape can have its own, and merged shapes blend them.
The surface is a SeaGlassFinish on the layer's settings
(LiquidGlassSettings.seaGlassFinish), shared by every shape:
| Control | What it does |
|---|---|
strength |
Cross-fades from regular glass (0) to sea glass (1). |
depth |
How far in from the rim the pebble reaches full thickness, in logical pixels. |
scatter |
The share of light the frost scatters. Scattered light leaves the glass in its body color, so it glows even over dark content. |
rimGlow |
Brightness of the frosted rim, strongest on the side facing the light. |
grain, grainSize |
The fine sugary texture of the pits. |
sparkle |
Tiny glints where single pits catch the light. |
pitting |
How far the pits scatter the backdrop, in logical pixels, on top of frost. |
gloss |
How brightly the dome catches the light: a curved glint on the shoulder facing it, a softer one on the far wall, and a thin bright line along the outline. The flat center stays untouched. |
stochasticFrost |
Renders frost in the glass pass by scattering samples, instead of a blur pass (default true, see below). |
SeaGlassFinish() keeps the backdrop readable. SeaGlassFinish.frosted() is
a milkier pebble (pair it with a larger frost, around 10) and
SeaGlassFinish.clear() a barely tumbled shard. SeaGlassFinish.shadows
returns a soft shadow tinted by the body, as colored glass casts on a light
surface; keep its elevation small for flat UI.
How sea glass differs from clear glass #
Apple's Liquid Glass is smooth, clear glass. Sea glass keeps its behavior (one shared backdrop sample, lensing, materializing with light rather than opacity, merging like droplets, a glow under the finger) but its physics differ:
- Refraction. A pebble's face is curved all over, not flat with a bevel,
so the
seaGlasspreset turnsrefractionFitsShapeoff and makes the lens a 100 pt dome: it is flat at the center and steepens toward the outline, so grid lines seen through a pebble bow outward toward its rim, as through a magnifying glass. Smaller pebbles bend proportionally less. The frost and the pits then smear what the lens bends. For a clear lens, useSeaGlassFinish.clear()withfrost: 0, and a littledispersion(around0.06) for rainbow edges. - Thickness. Larger glass behaves like thicker material: a large card absorbs more of its body color and scatters light more softly than a small button with the same finish.
- Lighting. A rough surface has no crisp highlight. Apple's glint is
dimmed; instead the light glints off the dome's shoulder facing it
(
gloss), the frosted rim glows on that side, and the pits are tiny facets that glint when they face it. - Merging. Shapes in a
LiquidGlassBlendGroupfuse like droplets, as on iOS. Between two pebbles the bridge is thin glass, so it is paler and frostier than either body, and the two body colors blend across it. Each shape's frost lies on that shape's own surface: it moves, turns and stretches with the shape, and cross-fades without a seam across a bridge. - Interaction.
LiquidStretch.seaGlassbehaves like a heavier drop than Apple's gel-like glass. It swells a little less when pressed and resists the drag more, but still stretches far enough to flow into a neighbor in a blend group, and it wobbles back when let go (wobble). In the example's quick settings, tiles pulled toward each other bridge and pinch off again.
Pebbles and living glass #
Sea glass is rarely a perfect oval. LiquidBlob is the ellipse that fills
its box with its radius rippled by up to three waves (LiquidWave), so the
outline stays within the box. LiquidBlob.pebble(seed: ...) is a tumbled,
lumpy pebble, different for every seed, and LiquidBlob.cookie a scalloped
one. Blobs refract, light, blend and work in FakeGlass like the other shapes.
Change the waves every frame and the glass lives: LiquidBlob.alive(time: t)
turns its waves at different speeds and swells them, so the outline flows and
breathes. Drive time from a ticker:
ValueListenableBuilder(
valueListenable: seconds, // updated by a Ticker
builder: (context, time, _) => LiquidGlass(
shape: LiquidBlob.alive(time: time),
appearance: LiquidGlassAppearance.seaGlass(color: SeaGlassColors.amber),
child: const SizedBox(width: 160, height: 150),
),
)
When a shape changes, only the geometry around it is drawn again, so one living pebble costs a fraction of the layer's matte per frame.
Light direction #
LiquidGlassSettings.lightAngle turns the light, in radians clockwise from
straight above. The glint, the dark border, the inner shadow, the sea glass
rim glow and the sparkles all follow it, and as it moves the sparkles
twinkle. 0 is the overhead light of iOS 27's resting glass. Apple moves some
highlights with the device; feed the device's tilt (for example from a
gyroscope) or a gesture into lightAngle to do the same. It is one uniform,
so changing it never re-renders the geometry.
Slab sea glass #
| Daylight | Flashlight |
|---|---|
![]() |
![]() |
A separate preset of thick, milky pastel glass slabs, inspired by the Sea Glass design concept (a community Figma file). It is an unofficial recreation, not affiliated with or endorsed by Google, and not a Google API. Where the pebbles above are domes that bow the backdrop, this glass is a thick, milky slab flecked like terrazzo. Its color is deepest just above the middle; it brightens along its thin outline and glows where the light that went through it gathers, along the edge away from the light. The thin rim is clear, so content scrolling behind shows through it, bent by the lens.
LiquidGlassLayer(
settings: LiquidGlassSettings.slabSeaGlass(),
child: LiquidGlass(
shape: const LiquidRoundedSuperellipse(borderRadius: 40),
appearance: SlabSeaGlass.appearance(SlabSeaGlass.original.primary),
child: const SizedBox(width: 320, height: 96),
),
)
SeaGlassFinish.slab() turns on five controls the other finishes leave
at 0, so they render as before:
| Control | Effect |
|---|---|
shading |
Shades the glass as a slab: the body color where it is thickest, brighter along the thin outline (except on the wall facing the light), a clear rim. Brightening saturates a color's strongest channels first, so blue glass pales toward cyan. |
glow |
The light that went through the slab gathers along the edge away from the light. |
flecks |
Angular chips, darker bits of the body and whitish crumbs, a point or two long. They move, turn and scale with their shape. |
shadow |
Each shape casts a soft shadow elevation points away from the light, with a band of its color where the light through the glass lands. |
wobble |
Sinks the outline inward in gentle undulations, up to 4 points, so shapes look hand made. It never grows a shape past its box. |
LiquidGlassSettings.light is the light those use (GlassLight). Without a
position it is daylight along lightAngle, straight above by default, as
on the concept's phone screens. With one, in global logical
pixels, it hangs height above the glass like a lamp: each shape is lit from
its own side and shadows fan out away from it. color and intensity set
the light; ambient lifts every shape, as a room's light does; reach makes
it fall off like a flashlight's beam. Moving the light only changes
uniforms, never the geometry.
LiquidGlassSettings.slabSeaGlass(
light: GlassLight(
position: pointerPosition,
height: 360,
reach: 560,
color: const Color(0xFFFFF0DC),
ambient: const Color(0xFF6F5F4D),
),
)
SlabSeaGlass holds the concept's four glass sets (original, beach,
jewel and frost), each a color per role: primary for search bars and
tiles, secondary for media cards, tertiary for navigation bars,
positive for selections, error for calls to action, accent for
floating action buttons, and so on. FakeGlass follows the shading, glow,
flecks, light and shadows, but does not refract; only full glass draws the
wobble.
The example recreates the concept's component sheet: a search bar that expands into an assistant answer, quick settings, a music card, segmented buttons, a floating action button menu, daylight, dusk and a flashlight that follows the pointer, and a tap on the sparkle cycles the glass sets:
cd example
flutter run -t lib/slab_sea_glass_main.dart --enable-impeller --enable-flutter-gpu
Cost #
The finish adds no textures. Its per-pixel work runs only while it is
enabled; SeaGlassFinish.none(), the default, leaves the renderer exactly as
without it.
With stochasticFrost (the default), the glass pass renders frost itself
with twelve backdrop samples spread over a disc at a random turn per pixel,
the way the pits scatter light. A sea glass layer then needs no blur pass, so
it skips that pass's intermediate textures and the command buffers it costs
(about four per frame on Metal, regardless of radius). The frost comes out
finely grained, as real frosted glass does. For a very heavy frost over a
large area, turn it off to use the Gaussian blur pass again.
FakeGlass follows the same model without reading the backdrop in a shader: its color filter applies the face of the thin rim, a multiply-blended thickness pass deepens the body toward the center, and its surface pass draws the rim glow, grain and sparkles.
Clear colored glass #
Real sea glass ranges from milky to nearly clear. SeaGlassFinish.colorClarity
clears glass of a color: it scatters, frosts and pits the backdrop less, so
it shows and bends what is beneath while keeping its color. Only colored
glass clears, by how much color it holds; frosted white and colorless glass
in the same layer keep their frost, and a bridge between the two clears
gradually. Even at 1 the glass keeps some of its milkiness and frost, so
it still reads as sea glass rather than tinted film.
LiquidGlassSettings.seaGlass(
finish: const SeaGlassFinish(colorClarity: .6),
)
It works with every finish, including slab glass. 0, the default, renders
as before.
Settings reference #
LiquidGlassSettings #
Shared by every shape in a layer.
| Setting | Default | What it does |
|---|---|---|
refractionHeight |
20 |
Width of the refracting rim, in logical pixels. Sea glass uses a 100 pt dome; slab glass a thin 12 pt rim. |
refractionAmount |
60 |
How far the rim bends the backdrop, in logical pixels. 0 turns refraction off. |
refractionFitsShape |
true |
Small shapes shrink the lens to fit, so it bends their rim instead of stretching the whole shape. |
backdropShrink |
0 |
Shows the backdrop seen through the face smaller, 0 to 0.75. |
frost |
2 |
Backdrop blur, in logical pixels. Raise it for frosted glass over photos. |
dispersion |
0 |
Rainbow edges: separates the color channels of the refracted rim. Negative values bend blue more, as real glass does. |
highlight |
1 |
Strength of the glint along the light axis. |
contourStrength |
0.43 |
The thin dark border just outside the outline. |
contourDirectionality |
0.77 |
How much that border concentrates where the glint fades. |
bevelShadowStrength |
0.036 |
The faint inner shadow cast by the rim. |
tintAmount |
0 |
The iOS 27 Liquid Glass slider, from Clear (0) to Tinted (1). |
lightAngle |
0 |
Where the light comes from, in radians clockwise from straight above. |
light |
daylight | A GlassLight: daylight along lightAngle, or a lamp at a position. |
seaGlassFinish |
none | The sea glass surface (SeaGlassFinish); none() is regular liquid glass. |
SeaGlassFinish #
The sea glass surface. The presets:
| Control | SeaGlassFinish() |
.frosted() |
.clear() |
.slab() |
What it does |
|---|---|---|---|---|---|
strength |
1 | 1 | 1 | 1 | Cross-fades from regular glass (0) to sea glass (1). |
depth |
18 | 22 | 14 | 14 | How far in from the rim the glass reaches full thickness, in logical pixels. |
scatter |
0.12 | 0.45 | 0.05 | 0.85 | How milky the glass is: the share of light it scatters in its body color. |
rimGlow |
0.35 | 0.5 | 0.25 | 0 | The frosted rim's glow, strongest facing the light. |
grain |
0.5 | 0.7 | 0.3 | 0.15 | The fine sugary texture of the pits. |
grainSize |
1.4 | 1.8 | 1.2 | 1.1 | One grain, in logical pixels; also the size of slab flecks. |
sparkle |
0.35 | 0.45 | 0.2 | 0 | Tiny glints where pits catch the light. |
pitting |
0.6 | 1.6 | 0.4 | 0.3 | How far the pits scatter the backdrop, in logical pixels. |
gloss |
0.6 | 0.35 | 0.85 | 0 | The glint on the dome's shoulder and outline. |
shading |
0 | 0 | 0 | 1 | Slab shading: the body color in the middle, a bright thin outline, a clear rim. |
glow |
0 | 0 | 0 | 1 | The pale glow along the edge away from the light. |
flecks |
0 | 0 | 0 | 1 | Terrazzo chips inside the glass, in tones of its color. |
shadow |
0 | 0 | 0 | 1 | The shadow each shape casts away from the light (0 turns it off). |
elevation |
12 | 12 | 12 | 18 | How high the glass stands above what it casts its shadow on. |
wobble |
0 | 0 | 0 | 1.5 | Hand-cut outlines that sink inward by up to this many points (full glass only). |
colorClarity |
0 | 0 | 0 | 0 | How much colored glass clears (see Clear colored glass). |
stochasticFrost |
true | true | true | true | Renders frost with scattered samples in the glass pass; turn it off for a smooth Gaussian frost. |
Change any control with copyWith:
LiquidGlassSettings.slabSeaGlass(
finish: const SeaGlassFinish.slab().copyWith(flecks: .35, shadow: 0),
)
LiquidGlassAppearance #
Per shape, or as a layer's defaultAppearance.
| Setting | What it does |
|---|---|
LiquidGlassAppearance.seaGlass(color:) |
The body color of sea glass. The color is what the glass shows over white at full thickness; its alpha is how much color it holds (0 is colorless frosted glass). |
tint |
The color of regular glass, mixed with the backdrop by its alpha. |
saturation, transmissionGamma, vibrancy |
Tone of the backdrop seen through regular glass. |
visibility |
Materializes the glass, from 0 (hidden) to 1. |
GlassLight #
The light of slab shading and cast shadows (LiquidGlassSettings.light).
| Setting | Default | What it does |
|---|---|---|
position |
none | Where a lamp hangs, in global logical pixels; without it the light is daylight along lightAngle. |
height |
600 |
How high the lamp hangs; lower lamps cast longer shadows. |
color, intensity |
white, 1 |
The light. |
ambient |
black | Light that reaches every shape, as a room's does. |
reach |
none | The radius over which a lamp fades to half, like a flashlight's beam. |
Recipes #
// Frosted white glass over a photo, colored glass a little clearer.
LiquidGlassSettings.seaGlass(
finish: const SeaGlassFinish(colorClarity: .5),
frost: 8,
)
// Clear slab: translucent pastel slabs that refract what is beneath.
LiquidGlassSettings.slabSeaGlass(
finish: const SeaGlassFinish.slab().copyWith(scatter: .24),
).copyWith(refractionHeight: 32, refractionAmount: 26, refractionFitsShape: true)
// Slab glass without shadows.
LiquidGlassSettings.slabSeaGlass(
finish: const SeaGlassFinish.slab().copyWith(shadow: 0),
)
// A flashlight that follows the finger.
LiquidGlassSettings.slabSeaGlass(
light: GlassLight(position: pointer, height: 320, reach: 480),
)
// A clear lens with rainbow edges.
LiquidGlassSettings.seaGlass(
finish: const SeaGlassFinish.clear(),
frost: 0,
dispersion: -.3,
)
Refraction #
Glass is modeled as a flat face with a rounded bevel along its edge. Only the bevel refracts; the face shows the backdrop undisplaced.
refractionHeight: the bevel width in logical pixels (iOS 27:20).refractionAmount: how far inside the silhouette the outermost pixel samples the backdrop (iOS 27:60). The displacement falls off across the bevel as a quarter circle. Above a ratio of 1 torefractionHeight, content near the rim is mirrored, as on Apple's glass.0turns refraction off.refractionFitsShape(defaulttrue): small shapes shrink the lens like iOS 27 regular glass. The bevel is at most a quarter of the short side, and the rim samples no deeper than the center line. Clear glass sets it tofalse.backdropShrink: shrinks the backdrop seen through the whole face.0keeps its size and0.08shows it at 92%. It never enlarges, so the glass never pixelates the backdrop. All glass in a layer shrinks about the center of the layer's glass; give a shape its own layer to shrink it about itself.dispersion: splits the colors in the refracted edge. Red moves by1 + dispersion / 2and blue by1 - dispersion / 2times the edge displacement; negative values bend blue more, as real glass does. iOS 27 regular and clear glass show none, so it defaults to0, where the glass reads the backdrop once per pixel instead of three times.
The backdrop is always sampled bilinearly, so refracted lines move smoothly instead of snapping to whole pixels.
Magnifiers #
backdropShrink never magnifies, because enlarging a captured backdrop makes
it blurry. The example playground shows how to build an
iOS 27 text loupe instead: it re-renders the content under the lens at the
magnified resolution and draws LiquidGlass.withOwnLayer on top, so text stays
sharp. The loupe is example code, not part of the package; copy
example/lib/loupe/liquid_glass_loupe.dart
if you need one.
| Light | Dark |
|---|---|
![]() |
![]() |
Glint #
The glint is the thin bright line along the rim, on the two walls facing along the light. It recolors the glass instead of adding white: glass over color glints in that color.
highlight sets its strength; 1 matches iOS 27 on an iPhone. The line is
1.2 pt wide, equally bright on both walls, and fades around corners as on
iOS 27.
Two more strengths shape the rim: contourStrength is the 0.75 pt dark border
just outside the silhouette (contourDirectionality concentrates it where the
glint fades), and bevelShadowStrength is the faint inner shadow the rim casts
on the face. The defaults and presets set all of them.
HDR #
The glint aims at a color brighter than SDR white. Full glass writes that value unclamped, so whether it reaches the display depends on the surface Flutter renders into:
- iOS: set
FLTEnableWideGamuttotrueinInfo.plist. The surface then holds values up to about 1.25, so the brightest part of the glint is compressed. Flutter's layer doesn't request extended dynamic range, so the values above 1.0 only reach the display once the app setswantsExtendedDynamicRangeContenton the Flutter view'sCAMetalLayer; the example app does this in its app delegate. - macOS: with
FLTEnableWideGamuton capable hardware, the surface keeps the full range. - Android: 8-bit surfaces, so the glint is SDR.
FakeGlass draws a neutral glint within SDR white.
Visibility #
Visibility is per shape, not a layer setting. Set
LiquidGlassAppearance.visibility for one shape, or animate
LiquidGlassVisibility around a subtree:
LiquidGlassVisibility(
visibility: animation.value,
child: const Row(
children: [
LiquidGlass(shape: LiquidOval(), child: SizedBox.square(dimension: 56)),
LiquidGlass(shape: LiquidOval(), child: SizedBox.square(dimension: 56)),
],
),
)
As visibility falls, the glass dissolves: refraction goes to zero, lighting and
blur fade, and the shape's child fades with it. Children stay mounted and
interactive. Nested scopes multiply (0.5 inside 0.4 gives 0.2). A layer
whose shapes are all invisible stops sampling the backdrop.
Don't fade glass with Opacity or FadeTransition between the layer and its
shapes; that only fades the children. Opacity above a whole
LiquidGlassLayer works.
FakeGlass #
| Full glass | FakeGlass |
|---|---|
![]() |
![]() |
![]() |
![]() |
Full glass bends the backdrop at the rim; FakeGlass keeps everything else.
FakeGlass renders glass with a backdrop filter instead of the Flutter GPU
pipeline. Layers use it automatically where full glass is unavailable (Skia,
the web, no Flutter GPU). Set fake: true on a layer to use it on purpose, or
to test that path.
It keeps frost, tint, the color model, the glint, the border, the inner
shadow, visibility, exterior shadows and blend groups. It leaves out
refraction, backdropShrink, dispersion and vibrancy.
A blend group joins the way full glass does. Each member's surface draws the group's smooth union where that member is the nearest, with up to three neighbors, and the bridges between members get their own clipped backdrop filter. All of them share the layer's one backdrop copy. Across a bridge the tint blends from one member to the next, but the backdrop's color transfer switches halfway.
FakeGlass is not a cheaper mode: it pays for the same backdrop readback and
blur as full glass (see below).
Glass on glass and LiquidGlassCapture #
Apple's guidance is not to stack glass on glass. Sometimes you have to, for example with an indicator that slides over its tab bar and should refract it.
Every LiquidGlassLayer is a BackdropFilter, and on Impeller each one copies
the whole render pass behind it, usually the entire screen. Two independent
layers pay that copy twice. You have four options:
| Backdrop copies | The top glass shows | Trade-off | |
|---|---|---|---|
Shapes in one LiquidGlassLayer |
1 | The content below | Shared settings; the shapes can't refract each other. |
Layers sharing a BackdropGroup (useBackdropGroup: true) |
1 | The content below, not the other glass | The indicator doesn't look like it sits on the bar. |
| Independent layers (default) | 1 per layer | The glass below | Cost grows with every layer. |
Layers inside a LiquidGlassCapture |
1 small copy for the capture | The glass below | Content the glass refracts must paint outside the capture. |
LiquidGlassCapture copies the backdrop once, only as large as the glass
inside it, and the layers inside read from that small copy. The result looks
the same as independent layers.
Stack(
children: [
content,
Align(
alignment: Alignment.bottomCenter,
child: LiquidGlassCapture(
child: LiquidGlassLayer(
// the bar
child: LiquidGlassLayer(
// the indicator, refracting the bar
),
),
),
),
],
)
The capture sizes itself to the glass inside plus its blur, refraction and
shadows. Pass bleed to size it yourself. A capture the size of the screen
saves nothing.
Performance #
The unit of cost is the backdrop copy, not the glass widget. Measured on a Pixel 10 (Impeller/Vulkan, 120 Hz, GPU power rail):
| Workload | GPU power |
|---|---|
Backdrop copy alone, per independent BackdropFilter |
~115 mW |
Plain BackdropFilter blur, σ7 |
~165 mW |
FakeGlass |
~230 mW |
| Full glass | ~335 mW |
Two full layers, independent vs. sharing a BackdropGroup |
797 vs. 688 mW |
| Glass shadow | ~75 mW per shape |
Best practices:
- Count backdrop copies per frame. Put siblings in one layer. Layers over
the same content can share one copy with
useBackdropGroup: trueor a sharedbackdropKey; shared members don't see what paints between them. In debug builds the package logs a warning when a frame makes more than one independent copy. - Keep layers small. A layer's cost grows with the area its glass covers.
- Keep moving glass small. Moving a shape redraws the geometry around it, out to three blend radii for a grouped shape. Static geometry is cached.
- Keep shadows few and small.
- Mind the blur. Impeller stops downsampling at σ ≤ 4, which makes small blurs cost more than σ7; σ20 costs about three times σ7.
- For low power, don't sample the backdrop.
FakeGlasspays the same copy and blur as full glass. When the device or power state calls for it, draw an opaque surface instead. - Call
LiquidGlass.precache()beforerunApp.
The full evidence is in the performance audit, and the Android GPU power harness measures your own screens.
Example playground #
cd example
flutter run --enable-impeller --enable-flutter-gpu
The playground has an iOS-style tab bar and toolbar, blending and color
scenes, the text loupe, a sea glass quick settings panel (under the Sea Glass
style the color scene turns into the sea glass palette: twelve pebbles that
merge, and one that is alive), every preset
in light and dark, the Liquid Glass slider, sea glass finish controls, a light
angle, full/fake switching and several backdrops. Settings you tune can be
saved as custom presets.
lib/slab_sea_glass_main.dart runs the slab sea glass
component sheet.
The showcase app #
example/lib/sea_glass_showcase_main.dart is a six-screen app made of sea
glass: a quick settings shade, a tide pool of living pebbles with a
draggable sun and a dome lens, a music player, weather, a smart home and a
wallet.
cd example
flutter run -t lib/sea_glass_showcase_main.dart --enable-impeller --enable-flutter-gpu
The palette button offers every sea glass color. The style button opens a
tray with every setting the README describes, applied to all screens at
once: the glass (sea glass, slab or clear slab), the wallpaper, the finish,
the tone, the shape of round buttons (round, pebble or living), the shine,
shadows on or off, and sliders for transparency, frost and color. Its
source in example/lib/sea_glass_showcase/ shows how to theme a whole app
from one style object.
Limitations #
- Experimental: not yet battle-tested in production apps.
- Full glass needs Impeller and Flutter GPU; the web renders
FakeGlass. - At most 16 shapes per layer.
- One corner radius per shape.
- A glass widget's child is painted on top of the glass, never refracted by it.
Contributors #
- ansalabs: creator and maintainer of flutter_sea_glass.
- Claude (Anthropic, through Claude Code): core contributor. Built much of flutter_sea_glass with the maintainer: clear colored glass, flecks inside slab glass, the shadow and rendering fixes, the sea glass showcase and the package split.
- Tim Lehmann (whynotmake.it): author of liquid_glass_renderer, which this package is built on, together with its contributors.














