native_liquid_glass 0.3.0
native_liquid_glass: ^0.3.0 copied to clipboard
Liquid Glass iOS platform views for Flutter using native UIKit components.
native_liquid_glass #
A Flutter plugin that exposes iOS native Liquid Glass widgets through Flutter Platform Views (UiKitView).
On iOS 26+, all widgets automatically adopt Apple's Liquid Glass visual style. On older iOS versions and non-iOS platforms, graceful fallbacks are provided.
Widgets #
| Widget | Native UIKit component |
|---|---|
LiquidGlassTabBar |
UITabBarController |
LiquidGlassButton |
UIButton with glass configurations |
LiquidGlassButtonGroup |
Row/column of UIButtons with unified glass blending |
LiquidGlassContainer |
Glass-effect UIView with custom shapes, animated transitions, and interactive press |
LiquidGlassNavigationBar |
UINavigationBar |
LiquidGlassToolbar |
UIToolbar |
LiquidGlassSearchBar |
Expandable UISearchTextField |
LiquidGlassSearchScaffold |
Full-screen scaffold with native tab bar + UISearchTab |
LiquidGlassToggle |
UISwitch |
LiquidGlassSlider |
UISlider |
LiquidGlassStepper |
UIStepper |
LiquidGlassSegmentedControl |
UISegmentedControl |
LiquidGlassColorPicker |
UIColorWell |
LiquidGlassDatePicker |
UIDatePicker |
LiquidGlassMenu |
UIButton + UIMenu context menu |
LiquidGlassSheet |
UISheetPresentationController |
LiquidGlassAlert |
UIAlertController |
LiquidGlassPopover |
UIPopoverPresentationController |
LiquidGlassActivityIndicator |
UIActivityIndicatorView |
LiquidGlassProgressView |
UIProgressView |
Requirements #
- Flutter 3.41.2+
- iOS/iPadOS 26.0+ for full Liquid Glass behavior (older iOS versions fall back to standard system styles)
Integration #
This plugin supports both CocoaPods and Swift Package Manager (SPM).
- CocoaPods (default) — works automatically with
flutter pub get - SPM — supported from Flutter 3.19+ via
ios/native_liquid_glass/Package.swift
Installation #
dependencies:
native_liquid_glass: ^0.2.7
Then run flutter pub get.
Icons #
All icon-bearing widgets accept NativeLiquidGlassIcon, which supports three sources:
NativeLiquidGlassIcon.sfSymbol('star.fill') // Native SF Symbol (preferred on iOS)
NativeLiquidGlassIcon.iconData(Icons.star) // Flutter IconData (PNG-encoded for iOS)
NativeLiquidGlassIcon.asset('assets/star.png') // App bundle asset (PNG or SVG)
On iOS, source resolution priority is asset → IconData → SF Symbol.
Usage #
LiquidGlassTabBar #
LiquidGlassTabBar(
items: const [
LiquidGlassTabItem(
label: 'Home',
icon: NativeLiquidGlassIcon.sfSymbol('house'),
selectedIcon: NativeLiquidGlassIcon.sfSymbol('house.fill'),
selectedItemColor: Color(0xFF007AFF),
iosBadgeValue: '3',
),
LiquidGlassTabItem(
label: 'Search',
icon: NativeLiquidGlassIcon.sfSymbol('magnifyingglass'),
),
LiquidGlassTabItem(
label: 'Profile',
icon: NativeLiquidGlassIcon.iconData(Icons.person_outline),
selectedIcon: NativeLiquidGlassIcon.iconData(Icons.person),
),
],
currentIndex: _selectedIndex,
onTabSelected: (index) => setState(() => _selectedIndex = index),
height: 72,
selectedItemColor: Colors.blue,
labelTextStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
)
LiquidGlassButton #
// Text button
LiquidGlassButton(
label: 'Continue',
onPressed: () {},
icon: NativeLiquidGlassIcon.sfSymbol('arrow.right'),
style: LiquidGlassButtonStyle.prominentGlass,
)
// Icon-only button
LiquidGlassButton.icon(
onPressed: () {},
icon: NativeLiquidGlassIcon.sfSymbol('heart.fill'),
tint: const Color(0xFFFF375F),
size: 50,
)
LiquidGlassButtonGroup #
LiquidGlassButtonGroup(
buttons: [
LiquidGlassButtonData(
label: 'Bold',
icon: NativeLiquidGlassIcon.sfSymbol('bold'),
onPressed: () {},
),
LiquidGlassButtonData(
label: 'Italic',
icon: NativeLiquidGlassIcon.sfSymbol('italic'),
onPressed: () {},
),
],
axis: Axis.horizontal,
spacing: 8,
)
LiquidGlassContainer #
// Basic container
LiquidGlassContainer(
config: const LiquidGlassConfig(
effect: LiquidGlassEffect.regular,
shape: LiquidGlassEffectShape.capsule,
tint: Color(0xFF007AFF),
interactive: true,
),
width: 200,
height: 80,
onTap: () => print('tapped'),
child: const Center(child: Text('Glass!')),
)
// Wraps child when width/height omitted (like Flutter Container)
LiquidGlassContainer(
config: const LiquidGlassConfig(shape: LiquidGlassEffectShape.rect),
child: Padding(
padding: EdgeInsets.all(20),
child: Text('Wraps content'),
),
)
// Custom SVG shape (hand-built)
LiquidGlassContainer(
config: LiquidGlassConfig(
shape: LiquidGlassEffectShape.custom,
customPath: [
LiquidGlassPathOp.moveTo(50, 0),
LiquidGlassPathOp.lineTo(150, 0),
LiquidGlassPathOp.lineTo(200, 60),
LiquidGlassPathOp.lineTo(150, 120),
LiquidGlassPathOp.lineTo(50, 120),
LiquidGlassPathOp.lineTo(0, 60),
LiquidGlassPathOp.close(),
],
customPathSize: Size(200, 120),
),
width: 200,
height: 120,
child: const Center(child: Text('Diamond')),
)
// Custom shape from an SVG path string (`d` attribute of <path>)
LiquidGlassContainer(
config: LiquidGlassConfig(
shape: LiquidGlassEffectShape.custom,
customPath: 'M50 0 L150 0 L200 60 L150 120 L50 120 L0 60 Z'
.toLiquidGlassPath(),
customPathSize: const Size(200, 120),
),
width: 200,
height: 120,
child: const Center(child: Text('Diamond')),
)
// Animated transitions between shapes/effects
LiquidGlassContainer(
config: LiquidGlassConfig(shape: _currentShape),
animateChanges: true, // spring transition on config changes
child: myContent,
)
LiquidGlassNavigationBar #
LiquidGlassNavigationBar(
title: 'Settings',
leadingItems: const [
LiquidGlassNavBarItem(
id: 'back',
icon: NativeLiquidGlassIcon.sfSymbol('chevron.left'),
label: 'Back',
),
],
trailingItems: const [LiquidGlassNavBarItem(id: 'done', label: 'Done')],
// Optional. Null (default) follows the device appearance; pass the app
// theme's brightness if your app drives its own ThemeMode.
brightness: Theme.of(context).brightness,
onItemTapped: (id) {},
)
LiquidGlassToolbar #
LiquidGlassToolbar(
items: const [
LiquidGlassToolbarItem(id: 'share', icon: NativeLiquidGlassIcon.sfSymbol('square.and.arrow.up')),
LiquidGlassToolbarSpacer(),
LiquidGlassToolbarItem(id: 'done', label: 'Done'),
],
onItemTapped: (id) {},
)
Controls #
// Toggle
LiquidGlassToggle(value: _isOn, onChanged: (v) => setState(() => _isOn = v))
// Slider
LiquidGlassSlider(value: _volume, min: 0, max: 1, onChanged: (v) => setState(() => _volume = v))
// Stepper
LiquidGlassStepper(value: _count, min: 0, max: 10, onChanged: (v) => setState(() => _count = v))
// Segmented control
LiquidGlassSegmentedControl(
labels: const ['Day', 'Week', 'Month'],
selectedIndex: _period,
onValueChanged: (i) => setState(() => _period = i),
)
Indicators #
LiquidGlassActivityIndicator(animating: _isLoading)
LiquidGlassProgressView(progress: _uploadProgress, progressTintColor: Colors.blue)
Search #
LiquidGlassSearchBar(
placeholder: 'Search',
expandable: true,
onChanged: (query) {},
onSubmitted: (query) {},
)
Pickers #
LiquidGlassColorPicker(
selectedColor: _selectedColor,
onColorChanged: (color) => setState(() => _selectedColor = color),
)
LiquidGlassDatePicker(
mode: LiquidGlassDatePickerMode.date,
initialDate: DateTime.now(),
onDateChanged: (date) {},
)
Context Menu #
LiquidGlassMenu(
items: const [
LiquidGlassMenuItem(
id: 'copy',
title: 'Copy',
icon: NativeLiquidGlassIcon.sfSymbol('doc.on.doc'),
),
LiquidGlassMenuItem(id: 'delete', title: 'Delete', isDestructive: true),
],
onItemSelected: (id) {},
label: 'Actions',
)
The trigger is a plain system UIButton; the Liquid Glass look belongs to the system menu popup that UIKit presents. Use one trigger per screen — multiple instances (e.g. one per ListView row) interfere with each other, because the native menu interaction competes with Flutter's gesture arena (#11). For per-row menus, use PopupMenuButton or CupertinoContextMenu.
Modals #
// Alert
final actionId = await LiquidGlassAlert.show(
context: context,
title: 'Delete?',
message: 'This cannot be undone.',
actions: [
const LiquidGlassAlertAction(id: 'delete', title: 'Delete', isDestructive: true),
const LiquidGlassAlertAction(id: 'cancel', title: 'Cancel', isCancel: true),
],
);
// Sheet
LiquidGlassSheet.show(
context: context,
title: 'Options',
detents: [LiquidGlassSheetDetent.medium],
);
// Popover
LiquidGlassPopover.show(
context: context,
builder: (_) => const Text('Popover content'),
anchorRect: Offset.zero & const Size(50, 50),
);
Overlay suppression #
Native glass platform views sit above Flutter's rendering surface. When Flutter shows overlays (showModalBottomSheet, showDialog, page transitions), the glass can bleed through.
This is handled automatically — every glass widget hides itself when its route is no longer current and restores when it is. Glass items inside the overlay stay visible. No setup required.
For custom overlays that don't go through the Navigator, use the manual API:
NativeLiquidGlassLifecycle.suppressGlassEffects();
await showMyCustomOverlay();
NativeLiquidGlassLifecycle.unsuppressGlassEffects();
Transitions that don't push a route #
The automatic path keys off ModalRoute.of(context).isCurrent, so it covers route pushes and popup routes — but not transitions that happen inside a single route. Switching tabs with an IndexedStack or a TabBarView keeps the same route, so nothing fires, and the glass material can flash a flat dark tone for a frame: it samples the content behind it, and mid-transition Flutter's surface isn't readable.
Wrap those transitions manually:
void _onTabChanged(int index) async {
await NativeLiquidGlassLifecycle.suppressGlassEffects();
setState(() => _index = index);
await Future<void>.delayed(_transitionDuration);
await NativeLiquidGlassLifecycle.unsuppressGlassEffects();
}
This trades the flash for a fade, which reads far better but does not eliminate the underlying limitation — the glass is a real UIView composited above Flutter's Metal layer, and the plugin cannot control what that layer exposes mid-transition.
Device qualification check #
if (NativeLiquidGlassUtils.supportsLiquidGlass) {
// Running on iOS 26+ — full Liquid Glass behavior active.
}
Spring Animation System #
Cupertino-style spring physics for Flutter animations, built on SpringSimulation.
Presets #
| Preset | Duration | Bounce | Use case |
|---|---|---|---|
LiquidGlassSpring.bouncy() |
500ms | 0.3 | Playful, expressive |
LiquidGlassSpring.snappy() |
500ms | 0.15 | Quick UI transitions |
LiquidGlassSpring.smooth() |
500ms | 0.0 | Critically-damped, no overshoot |
LiquidGlassSpring.interactive() |
150ms | 0.14 | Tracking a pointer |
SpringBuilder #
SpringBuilder(
value: _expanded ? 1.5 : 1.0,
spring: LiquidGlassSpring.bouncy(),
builder: (context, value, child) {
return Transform.scale(scale: value, child: child);
},
child: myWidget,
)
VelocitySpringBuilder (drag + release) #
VelocitySpringBuilder(
value: _dragOffset,
active: _isDragging,
springWhenActive: LiquidGlassSpring.interactive(),
springWhenReleased: LiquidGlassSpring.snappy(),
builder: (context, value, velocity, child) {
return Transform.translate(offset: Offset(value, 0), child: child);
},
child: myWidget,
)
Controllers (imperative API) #
final ctrl = SingleSpringController(
vsync: this,
spring: LiquidGlassSpring.snappy(),
initialValue: 0.0,
);
ctrl.animateTo(1.0); // spring to target, preserving velocity
ctrl.setValue(0.5); // instant jump
SVG Path Utility #
SvgPathExtension on String parses SVG path data (the d attribute of an <path> element) into Flutter paths or LiquidGlassConfig.customPath ops — so you can take an SVG straight from a design tool and drop it into a glass container.
// Flutter Path
final path = 'M10 10 L20 20 Z'.toPath();
// Scaled Flutter Path (from SVG viewBox to target size)
final scaled = 'M10 10 L20 20 Z'.toPathScaled(
viewBox: const Size(30, 30),
target: const Size(120, 120),
);
// Direct to LiquidGlassConfig.customPath
LiquidGlassContainer(
config: LiquidGlassConfig(
shape: LiquidGlassEffectShape.custom,
customPath: 'M50 0 L150 0 L200 60 L150 120 L50 120 L0 60 Z'
.toLiquidGlassPath(),
customPathSize: const Size(200, 120),
),
child: ...,
)
// Handles non-zero-origin viewBoxes (e.g. "1 0 24 226")
LiquidGlassContainer(
config: LiquidGlassConfig(
shape: LiquidGlassEffectShape.custom,
customPath: svgD.toLiquidGlassPathScaled(
viewBox: const Rect.fromLTWH(1, 0, 24, 226),
target: const Size(48, 452),
),
customPathSize: const Size(48, 452),
),
child: ...,
)
Leading whitespace, newlines, comments, or stray characters before the first M/m are stripped automatically, so pasted SVG strings "just work". Quadratic Béziers are normalized to cubic Béziers to match iOS's path rendering.
How it works #
Each widget creates a UiKitView that Flutter embeds into the render tree. The iOS plugin registers a native FlutterPlatformViewFactory for each view type. Initial configuration is passed through creationParams; subsequent changes are pushed over a per-view FlutterMethodChannel.
On iOS 26+, all native UIKit controls automatically receive Apple's Liquid Glass styling. On older iOS versions, the same controls render with their standard system appearance.
Performance #
Every widget in this package is a real UIView hosted through Flutter's UiKitView. That is what makes the glass genuinely native, and it is also a cost that a Flutter-drawn widget does not have:
- Compositing. Flutter cannot draw a native view into its own surface. While a platform view is on screen, Flutter splits its output around it and keeps its frames in step with UIKit's view hierarchy on the main thread, so rasterization no longer runs fully in parallel with your Dart and UIKit work. This affects the whole screen for as long as any platform view is visible, not only the platform view itself.
- Overlay surfaces. Any Flutter content painted on top of a platform view (later in paint order and overlapping its rect) forces an extra overlay surface. Keep glass views last in the paint order —
Scaffold.bottomNavigationBar, or the final child of aStackwith nothing drawn over it — so no overlay is needed. - Glass sampling. On iOS 26+ the material samples the content behind it every frame. That is GPU work UIKit does for its own bars too, but it is not free.
What helps in practice:
- One or two platform views per screen, not one per list row. Prefer Flutter widgets for anything that repeats.
- Nothing painted over the glass view. If content must scroll under a bottom bar, make sure it is under it in paint order, not over it.
- Measure with the DevTools performance overlay / timeline on a release build on a device; simulator and debug numbers are not representative for platform views.
If a page is noticeably janky with a glass bar and smooth without it, and none of the above applies, please open an issue with a DevTools timeline capture.
Notes #
- Tab bar background, shadow, and badge styling are intentionally system-driven.
- Widgets that require custom icon data (non-SF Symbol) rasterize or load assets asynchronously before the platform view is shown; a same-size placeholder is displayed during resolution.
- See
example/for a working demo of all widgets.
Happy building.
Thanks for trying native_liquid_glass.