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')],
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',
)
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();
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.
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.