liquid_design
iOS 26 Liquid Glass for any Flutter widget, plus ready-made iOS 26 components.


| Platform | What you get |
|---|---|
| iOS 26+ | Real native glass (UIGlassEffect, UIGlassContainerEffect) |
| macOS 26+ | Real native glass (NSGlassEffectView, NSGlassEffectContainerView) |
| iOS 15–25 / macOS 10.15–15 | Treated like Android: your widget as-is, or the optional frosted blur (no native views) |
| Android, web, Windows, Linux | Installs and runs without errors; your widget as-is, or an optional frosted blur |
New in 0.4.0: buttons press and drag like native iOS 26: the glass follows the finger and stretches toward it (Buttons). 0.3.0: a picture and a short example for every component below. 0.2.x:
LiquidGlassSvg, glass in the exact shape of any SVG, and older iOS / macOS behave like Android.
Install
dependencies:
liquid_design: ^0.4.0
import 'package:liquid_design/liquid_design.dart';
LiquidGlass — wrap anything
LiquidGlass(
child: GestureDetector(
onTap: () {},
child: Container(
width: 56,
height: 56,
decoration: const BoxDecoration(shape: BoxShape.circle), // shape detected
child: const Icon(Icons.add),
),
),
)
- The child defines the size and should have a transparent background.
- Glass has no shadow: iOS 26 glass draws a soft shadow around itself, and the package clips every glass surface (single, grouped, lens) to its own shape so nothing is drawn outside it.
- Taps are handled by your child (
GestureDetector,InkWell, any button).LiquidGlassonly observes touches to animate: grow on press, stretch toward a drag, glow under the finger, spring back on release. - Shape is detected from
Container/DecoratedBox/ClipOval/ClipRRect(also throughGestureDetector/InkWell), or setshape: LiquidGlassShape.roundedRect(20). Default: capsule. - Inside a list, the press lets go as soon as the list starts scrolling.
Shorthand: call .liquidGlass() on any widget. It takes the same
parameters as LiquidGlass:
const Icon(Icons.add).liquidGlass(shape: const LiquidGlassShape.circle())
Text('Clear').liquidGlass(style: LiquidGlassStyle.clear, tintColor: Colors.blue)
Shapes

LiquidGlass(shape: const LiquidGlassShape.capsule(), child: label) // default
LiquidGlass(shape: const LiquidGlassShape.circle(), child: icon)
LiquidGlass(shape: const LiquidGlassShape.roundedRect(16), child: card)
LiquidGlass(shape: const LiquidGlassShape.rect(), child: banner)
LiquidGlass(shape: LiquidGlassShape.svg(starSvg), child: box) // any SVG
LiquidGlassSvg.asset('assets/logo.svg', height: 80) // SVG widget
Styles

LiquidGlass(child: card) // regular
LiquidGlass(style: LiquidGlassStyle.clear, child: card) // clear
LiquidGlass(tintColor: Colors.blue, tintOpacity: 0.4, child: card) // tinted
LiquidGlassSettings — every setting in one object
LiquidGlassSettings holds every glass setting. These are all of them,
with their defaults:
const settings = LiquidGlassSettings(
enabled: true, // false = plain child, no glass
style: LiquidGlassStyle.regular, // regular / clear
opacity: 1.0, // 0..1, how visible the glass is
tintColor: null, // e.g. Colors.blue for coloured glass
tintOpacity: 0.3, // 0..1, strength of the tint
interactive: true, // press / drag / hover motion
interactionStrength: 1.0, // 0 = none, 1 = iOS, >1 stronger
brightness: LiquidGlassBrightness.auto, // auto / system / light / dark
shape: LiquidGlassShape.capsule(), // default shape when not detected
fallback: LiquidGlassFallback.none, // none / frosted (Android, web, …)
fallbackBlurSigma: 18, // blur of Flutter-drawn glass
collapseOnScroll: true, // tab bar shrinks while scrolling
renderer: LiquidGlassRenderer.native, // native / flutter / auto
);
Apply it in one of three places:
// 1. The whole app
LiquidGlassService.instance.settings = settings;
// or change one value
LiquidGlassService.instance.update((s) => s.copyWith(opacity: 0.8));
// 2. One subtree (a screen, a sheet, ...)
LiquidGlassTheme(settings: settings, child: const MyScreen());
// 3. One widget
LiquidGlass(settings: settings, child: const MyButton());
// or single values straight on the widget
LiquidGlass(opacity: 0.6, tintColor: Colors.blue, child: const MyButton());
Components (LiquidGlassButton, LiquidGlassNavigationBar,
LiquidGlassSwitch, …) take a settings: too. Resolution order: the
widget's own values → its settings: → the nearest LiquidGlassTheme →
LiquidGlassService. Every change applies right away, with no reload.
| Setting | Type | Default | What it does |
|---|---|---|---|
enabled |
bool |
true |
Turns the glass on / off; off shows the child only |
style |
LiquidGlassStyle |
regular |
regular frosted glass or more transparent clear glass |
opacity |
double |
1.0 |
How visible the glass material is (0–1) |
tintColor |
Color? |
null |
Coloured glass, like iOS tinted buttons |
tintOpacity |
double |
0.3 |
Strength of the tint (0–1) |
interactive |
bool |
true |
Grow on press, stretch on drag, glow on hover |
interactionStrength |
double |
1.0 |
Motion amount: 0 = none, 1 = iOS, >1 stronger |
brightness |
LiquidGlassBrightness |
auto |
auto follows your app theme; or system, light, dark |
shape |
LiquidGlassShape |
capsule() |
Shape used when it can't be detected from the child |
fallback |
LiquidGlassFallback |
none |
frosted shows a Flutter glass on Android, web and desktop |
fallbackBlurSigma |
double |
18 |
Blur of Flutter-drawn glass (0 = no blur) |
collapseOnScroll |
bool |
true |
Navigation bar shrinks while scrolling down |
renderer |
LiquidGlassRenderer |
native |
native system glass, or flutter / auto Flutter-drawn glass |
LiquidGlassSvg — glass in the shape of any SVG
Turn a logo, an icon or any SVG into a Liquid Glass surface whose outline follows the artwork exactly: not a circle, not a capsule, the real shape.
| Platform | What LiquidGlassSvg shows |
|---|---|
| iOS 26+ / macOS 26+ | Native Liquid Glass cut to the SVG outline (SwiftUI glassEffect(in:)) |
| iOS 15–25 / macOS 10.15–15 | Same as Android: the normal SVG |
| Android, web, Windows, Linux | The normal SVG, drawn with its own colours (or Flutter glass with fallback: frosted) |
Native glass is used only where the system has Liquid Glass (iOS / macOS 26+). Everywhere else the package steps aside, exactly as on Android.
The rectangular platform view is never visible: the glass and its shadow are masked to the outline, including during light / dark switches.
From an asset
flutter:
assets:
- assets/logo.svg
LiquidGlassSvg.asset(
'assets/logo.svg',
height: 40, // width follows the SVG aspect ratio
tintColor: const Color(0xFF3E205A),
tintOpacity: 0.35,
semanticLabel: 'Hareer',
)
The file is loaded once and cached, so rebuilds never flash.
From a string
A full SVG document:
const star = '''
<svg viewBox="0 0 24 24">
<polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02
12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"/>
</svg>''';
LiquidGlassSvg(svg: star, width: 96, tintColor: Colors.amber)
Or only the path data (d):
LiquidGlassSvg(
svg: 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3'
'c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 '
'22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
width: 96,
)
Tap, press and content inside the shape
LiquidGlassSvg.asset(
'assets/logo.svg',
height: 110,
interactive: true, // grows and stretches under the finger
onTap: () => openHome(), // only taps inside the outline count
color: Colors.white, // plain-SVG colour on Android / web
child: const Center(child: Text('Hi')), // clipped to the outline
)
Stroke-only artwork
Line icons with fill="none" and a stroke are turned into a filled
outline, so the glass has the width of the stroke:
LiquidGlassSvg(svg: waveIcon, width: 130) // strokeToFill: true
LiquidGlassSvg(svg: waveIcon, width: 130, strokeToFill: false) // raw path as fill
As a shape for LiquidGlass
LiquidGlassShape.svg and LiquidGlassShape.path work anywhere a shape is
accepted:
LiquidGlass(
shape: LiquidGlassShape.svg(starSvg),
child: const SizedBox(width: 100, height: 100),
)
const SizedBox(width: 110, height: 90).liquidGlass(
shape: LiquidGlassShape.path(
Path()
..moveTo(0, 50)
..quadraticBezierTo(50, -20, 100, 50)
..quadraticBezierTo(50, 120, 0, 50)
..close(),
),
)
Parameters
| Parameter | Type | Default | What it does |
|---|---|---|---|
svg / assetName |
String |
required | SVG document, path data, or an asset path (.asset) |
width, height |
double? |
SVG size | Give one and the other follows the aspect ratio |
fit |
BoxFit |
contain |
How the artwork fits the box |
alignment |
Alignment |
center |
Where the artwork sits in the box |
tintColor |
Color? |
settings | Coloured glass, e.g. your brand colour |
tintOpacity |
double? |
settings | Strength of the tint (0–1) |
color |
Color? |
null |
Paints the plain SVG in one colour (non-glass platforms) |
rimColor, rimWidth |
Color?, double |
white, 1.0 |
Edge highlight of Flutter-drawn glass |
strokeToFill |
bool |
true |
Outline stroke-only artwork |
settings |
LiquidGlassSettings? |
theme / service | Per-widget glass settings |
interactive |
bool |
false |
Press / drag motion |
onTap |
VoidCallback? |
null |
Tap inside the outline |
semanticLabel |
String? |
null |
Screen-reader label |
child |
Widget? |
null |
Content drawn inside the shape (clipped) |
bundle |
AssetBundle? |
DefaultAssetBundle |
Bundle for .asset |
Supported SVG
path (every command, arcs included), rect (rounded), circle,
ellipse, line, polyline, polygon, nested g / svg, transform,
viewBox, presentation attributes, style="", <style> CSS classes,
fill-rule, opacity, and linear / radial gradients. Several shapes are
merged into one outline. No extra dependency: the parser is built in.
Components
Ready-made iOS 26 controls. Each one works with the same settings as
LiquidGlass (settings:, LiquidGlassTheme, LiquidGlassService).
| Component | Widget |
|---|---|
| Buttons | LiquidGlassButton, LiquidGlassButton.icon |
| Segmented control | LiquidGlassSegmentedControl |
| Switch and slider | LiquidGlassSwitch, LiquidGlassSlider |
| Search bar | LiquidGlassSearchBar |
| Bottom sheet | showLiquidGlassBottomSheet, LiquidGlassSheet |
| Navigation bar | LiquidGlassNavigationBar |
| App bar | LiquidGlassAppBar |
Buttons

Three styles, as text or icon buttons:
LiquidGlassButton(onPressed: save, child: const Text('Glass'))
LiquidGlassButton(
style: LiquidGlassButtonStyle.tinted,
onPressed: save,
child: const Text('Tinted'),
)
LiquidGlassButton(
style: LiquidGlassButtonStyle.prominent,
onPressed: buy,
child: const Text('Prominent'),
)
LiquidGlassButton.icon(onPressed: share, icon: const Icon(Icons.ios_share_rounded))
color and foregroundColor change the colours, shape the outline
(capsule by default), and onLongPress adds a long press.
Press and drag like a native iOS 26 button: the glass grows under the
finger, follows it with a rubber-band pull and stretches toward it. Letting
go within 70 pt of the button still taps it (like UIControl); drag further
and the glass springs back and the tap is cancelled. Inside a list, a
vertical drag scrolls the list instead.
Segmented control

LiquidGlassSegmentedControl<String>(
value: period,
onValueChanged: (v) => setState(() => period = v),
children: const {
'd': Text('Day'),
'w': Text('Week'),
'm': Text('Month'),
'y': Text('Year'),
},
)
Switch and slider

LiquidGlassSwitch(value: on, onChanged: (v) => setState(() => on = v))
LiquidGlassSlider(
value: volume,
onChanged: (v) => setState(() => volume = v),
divisions: 10, // optional
)
Switch, slider and segmented thumbs lift into a glass lens while touched. They work inside scrolling lists: a vertical swipe that starts on a switch, slider or segmented control scrolls the list and never changes the value. A sideways swipe or a tap changes it. Dragging past either end pulls the lens a little beyond the edge, with a rubber-band feel, and it springs back on release. A fast slider drag stretches the lens in the direction of travel.
Search bar

LiquidGlassSearchBar(
hintText: 'Search photos',
onChanged: filter,
onSubmitted: search,
)
Bottom sheet

showLiquidGlassBottomSheet(
context: context,
builder: (context) => const ShareMenu(),
);
Or place a LiquidGlassSheet(child: ...) yourself.
Navigation bar

LiquidGlassNavigationBar(
currentIndex: tab,
onTap: (i) => setState(() => tab = i),
onReselect: (i) => scrollToTop(i), // current tab tapped again
items: const [
LiquidGlassNavItem(icon: Icon(Icons.home_rounded), label: 'Home'),
LiquidGlassNavItem(icon: Icon(Icons.search_rounded), label: 'Search'),
LiquidGlassNavItem(icon: Icon(Icons.favorite_rounded), label: 'Saved'),
LiquidGlassNavItem(icon: Icon(Icons.person_rounded), label: 'Profile'),
],
)
The selection pill slides with a liquid stretch. Pressing lifts it into a glass lens that follows the finger (with haptic ticks) and snaps on release.
Icons only
![]()
Leave out label and the bar gets shorter (56 instead of 64):
items: const [
LiquidGlassNavItem(icon: Icon(Icons.home_rounded)),
LiquidGlassNavItem(icon: Icon(Icons.search_rounded)),
LiquidGlassNavItem(icon: Icon(Icons.favorite_rounded)),
LiquidGlassNavItem(icon: Icon(Icons.person_rounded)),
],
activeIcon shows a different icon for the selected tab.
With an action button

Row(
children: [
Expanded(child: LiquidGlassNavigationBar(/* ... */)),
const SizedBox(width: 12),
LiquidGlassButton.icon(onPressed: compose, icon: const Icon(Icons.add_rounded)),
],
)
Collapsed while scrolling

Like iOS 26, the bar can shrink to the current tab while the user scrolls down, and open again when tapped:
LiquidGlassScrollCollapse(
onChanged: (c) => setState(() => collapsed = c),
child: ListView(/* ... */),
)
LiquidGlassNavigationBar(
collapsed: collapsed,
onExpand: () => setState(() => collapsed = false),
/* ... */
)
Users can turn this off with
LiquidGlassService.instance.setCollapseOnScroll(false): the bar then stays
open, and opens right away if it was collapsed.
Tinted and custom colours

LiquidGlassNavigationBar(
activeColor: Colors.white,
inactiveColor: Colors.white70,
settings: LiquidGlassService.instance.settings.copyWith(
tintColor: const Color(0xFF6A1B9A),
tintOpacity: 0.55,
),
/* ... */
)
Dark mode

Nothing to do: with brightness: LiquidGlassBrightness.auto (the default)
the glass follows your app theme.
App bar

Scaffold(
extendBodyBehindAppBar: true,
appBar: LiquidGlassAppBar(
title: const Text('Inbox'),
actions: [
IconButton(onPressed: search, icon: const Icon(Icons.search)),
IconButton(onPressed: more, icon: const Icon(Icons.more_horiz_rounded)),
],
),
body: ListView(/* ... */),
)
Floating glass buttons (automatic back button; several actions share one capsule) and a scroll-edge fade once content scrolls underneath.
Groups: morphing and performance

LiquidGlassGroup(
spacing: 24,
child: Row(mainAxisSize: MainAxisSize.min, children: [
LiquidGlassButton.icon(onPressed: edit, icon: const Icon(Icons.edit_rounded)),
const SizedBox(width: 8),
LiquidGlassButton.icon(onPressed: crop, icon: const Icon(Icons.crop_rounded)),
const SizedBox(width: 8),
LiquidGlassButton(onPressed: done, child: const Text('Done')),
]),
)
Everything inside a LiquidGlassGroup is drawn by one native glass
container:
- Morphing: shapes closer than
spacingmelt together and split apart like liquid as they move. - Performance: one native view instead of one per widget. In the example benchmark (40 glass chips scrolling, iOS 26.3 simulator): grouped 62 fps, 2.1 ms raster vs individual 46 fps, 5.1 ms raster. These are simulator numbers; measure on a device for your app.
Group glass is drawn beneath all Flutter content in the group:
- The background the glass shows must be outside the group (a group wrapping a list of cards with their own backgrounds hides the glass behind the cards).
- Hide the content of shapes that tuck behind others (see the example's morph demo).
LiquidGlass(joinGroup: false)keeps a glass out of an enclosing group.- Glass can grow
overflowpoints (default 24) past the group's bounds, e.g. a button pressed at the edge.
Use LiquidGlassGroup(spacing: 0, ...) to share one native view between
neighbouring glass without merging them. LiquidGlassAppBar does this for
its buttons by default (groupGlass).
Settings
final glass = LiquidGlassService.instance;
glass.setOpacity(0.8); // 0..1
glass.setInteractionStrength(1.4); // 0 = no motion, 1 = iOS, >1 stronger
glass.setStyle(LiquidGlassStyle.clear); // regular / clear
glass.setTint(Colors.blue, opacity: 0.3); // null removes the tint
glass.setBrightness(LiquidGlassBrightness.auto); // auto / system / light / dark
glass.setFallback(LiquidGlassFallback.frosted); // non-Apple platforms
glass.setCollapseOnScroll(false); // keep navigation bars open
glass.setRenderer(LiquidGlassRenderer.native); // native / flutter / auto
glass.setEnabled(false);
Renderer: native or Flutter-drawn glass
LiquidGlassRenderer.native (default) uses the real iOS 26 / macOS 26 glass
everywhere. It is also the fastest option on iPhone.
LiquidGlassRenderer.flutter draws glass with Flutter: a blur with a colour
boost, a light sheen and a specular rim. Use it only where a native view
can't go, for example inside a ShaderMask or ColorFiltered. Flutter blurs
can't be cached, so every frame of any animation on screen re-blurs every
Flutter glass. LiquidGlassRenderer.auto uses native glass except inside
scroll views.
Measured on an iPhone at 120 Hz in profile mode (missed frames while scrolling the example's Controls page / toggling a switch):
| renderer | scroll | switch |
|---|---|---|
| native | 57 | 0 |
| auto | 189 | 147 |
| flutter | 281 | 181 |
LiquidGlass(renderer: LiquidGlassRenderer.flutter, child: ...) // per widget
LiquidGlassService.instance.setRenderer(LiquidGlassRenderer.native);
fallbackBlurSigma sets the blur of Flutter-drawn glass (0 = no blur, the
cheapest).
Settings resolve in this order: a widget's own parameters → its settings:
→ the nearest LiquidGlassTheme → LiquidGlassService.
LiquidGlassTheme(
settings: LiquidGlassService.instance.settings.copyWith(style: LiquidGlassStyle.clear),
child: const PhotoViewer(),
)
Light / dark: with LiquidGlassBrightness.auto (default) glass follows
your app theme and switches instantly. For an instant app-wide switch, also
set themeAnimationDuration: Duration.zero on MaterialApp.
Capabilities: LiquidGlassService.instance.isLiquidGlassSupported is
true on iOS / macOS 26+. It is filled in once the first glass widget builds,
or await LiquidGlassService.instance.ensureInitialized().
Accessibility & RTL
- Right-to-left: navigation bar, segmented control, switch and slider mirror correctly; dragging selects what is under the finger.
- Reduce Motion: no stretching or bouncing, only calm transitions.
- Increase Contrast: the frosted fallback becomes more opaque. Native glass follows Reduce Transparency / Increase Contrast by itself.
- Screen readers: tabs and segments are selectable buttons, switches
report their state, sliders support increase / decrease, buttons accept a
semanticLabel. The glass itself is hidden from the accessibility tree. - Buttons keep Apple's 44 pt minimum touch target.
Performance tips
- Keep tab pages alive (e.g. an
IndexedStackwhose pages are built on first visit and cached). Rebuilding a page on every tab switch recreates all its native glass views right as the tab bar animates. The example app does this: a tab switch costs ~4 ms instead of ~100 ms (debug build). - Every separate glass view on screen adds work to every frame on iOS.
Flutter draws the content above each native view (a button's label, for
example) into its own full-screen layer. It redraws those layers on every
frame of any animation, and creates or drops them as views scroll in and
out. Group glass that sits on the same background in a
LiquidGlassGroup(spacing: 0, ...): one native view and one layer for the whole group. Bars, toolbars, rows of buttons and chip grids all fit. Glass in a group refracts what is painted before the group (the panel or page behind it), so put the group inside that background. The example groups its tab bar and the button panels on the Components page. (Measured: the Groups page scrolls with ~1 missed frame; pages with 10+ separate glass views miss 50–150.) - Switch, slider, segmented control and navigation bar lenses are created on first touch and leave the scene when the touch ends, so idle controls cost no native view.
- The lens follows the finger closely: native tracking catches up in 50 ms and the Flutter spring lags only about 30 ms.
- On iOS / macOS those lenses are animated by Core Animation: Flutter only sends a target on each touch event and the system animates at the display's refresh rate (up to 120 Hz), smooth even if Flutter drops a frame. Dragging across the tab bar went from ~30 to ~60 visible fps in the simulator.
- Glass animations never jump after a slow frame: each frame advances them by at most 1/30 s.
- Judge smoothness in profile mode on a device (
flutter run --profile); debug builds, especially on the simulator, are many times slower.example/integration_test/perf_test.dartmeasures frame times:
flutter drive --driver=test_driver/integration_test.dart --target=integration_test/perf_test.dart
Notes
-
On iOS 15–25 / macOS before 26 there is no Liquid Glass, so the package behaves like Android: no native views, your widgets as-is (or Flutter glass with
fallback: LiquidGlassFallback.frosted). -
example/integration_test/feature_test.dartwalks through every feature on a device or simulator. -
Without a group, each
LiquidGlasson iOS / macOS is a native platform view: fine for bars, buttons and floating controls. UseLiquidGlassGroupfor many glass shapes (lists, grids, toolbars). -
Glass inside a group ignores
Opacityancestors (the group draws it); use theopacitysetting instead.
👨💻 Author
Mohammed Al-Jaf: made with 💙 for the Flutter community.
Questions, ideas or feedback? Message me on Instagram or open an issue. If this package helps you, a 👍 on pub.dev and a ⭐ on GitHub are much appreciated.