liquid_glass_widgets 0.12.1
liquid_glass_widgets: ^0.12.1 copied to clipboard
A Flutter UI kit implementing the iOS 26 Liquid Glass design language. Features shader-based glassmorphism, physics-driven jelly animations, and dynamic lighting.
Liquid Glass Widgets #
Bring Apple's iOS 26 Liquid Glass to your Flutter app — a comprehensive glass widget library with real shader-based blur, physics-driven jelly animations, and dynamic lighting. Works on every platform out of the box.
https://github.com/user-attachments/assets/2fe28f46-96ad-459d-b816-e6d6001d90de
Wanderlust — a luxury travel showcase built entirely with liquid_glass_widgets
Features #
- Comprehensive glass widget library — containers, interactive controls, inputs, feedback, overlays, and navigation surfaces (see Widget Categories)
- Liquid Morph Engine — a standalone physics system powering iOS 26-style liquid morphing.
GlassMenuis the first consumer; future widgets will use the same engine for consistent liquid transitions. Seedocs/LIQUID_MORPH_ENGINE.md - Real frosted glass — native two-pass Gaussian blur + shader refraction on Impeller; lightweight shader on Skia/Web
- Just works everywhere — iOS, Android, macOS, Web, Windows, Linux; rendering path chosen automatically
- Adaptive quality (experimental) —
GlassAdaptiveScopebenchmarks the device at startup and adjusts quality in real time:minimalon slow hardware,standardon mid-range,premiumon fast devices. Degrades on thermal throttle, recovers when cool - Zero dependencies — no third-party runtime libraries, just the Flutter SDK
- One-line setup —
LiquidGlassWidgets.wrap(child: myApp)handles shader prewarming, accessibility bridging, root backdrop sharing, and global theming; addGlassBackdropScopeper screen to prevent ghost artifacts on navigation (see Backdrop Isolation) - Gyroscope lighting —
GlassMotionScopedrives specular highlights from anyStream<double> - WCAG-compliant by default — Reduce Motion and Reduce Transparency are respected automatically; no setup required
Examples #
Wanderlust — Luxury Travel Showcase #
A premium app demonstrating liquid_glass_widgets in a real-world production context — full-bleed imagery, parallax scroll, hero transitions, and a concierge chat interface. This is the app shown in the video above.
cd example/showcase && flutter pub get && flutter run
Apple Music Demo — iOS 26 Replica #
A recreation of the Apple Music app demonstrating GlassSearchableBottomBar, a floating playback pill, and the full iOS 26 navigation model with smooth morphing transitions.
cd example && flutter pub get && flutter run -t lib/apple_music/apple_music_demo.dart
Apple Messages Demo — iOS 26 Replica #
A replica showcasing the Liquid Morph Engine via GlassMenu. Tap the menu or Edit button at the top to see the teardrop open/close physics live.
cd example && flutter pub get && flutter run -t lib/apple_messages/apple_messages_demo.dart
Apple News Demo — iOS 26 Replica #
A recreation of the Apple News app demonstrating GlassSearchableBottomBar with its morphing search pill, category chips, hero cards, and rounded article tiles.
cd example && flutter pub get && flutter run -t lib/apple_news/apple_news_demo.dart
Widget Showcase — Full Component Library #
A complete catalogue of every glass widget organised by category. Use it to explore components, try live settings, and copy patterns directly into your app.
cd example && flutter pub get && flutter run
Component Demos — Copy-Pasteable Examples #
Eight focused, self-contained demos — one widget, one file, runnable standalone:
| Demo | Run command (from example/) |
|---|---|
glass_menu_demo.dart — all 9 menu alignments |
cd example && flutter run -t lib/demos/glass_menu_demo.dart |
glass_tab_bar_scrollable_demo.dart — scrollable tab bar |
cd example && flutter run -t lib/demos/glass_tab_bar_scrollable_demo.dart |
glass_modal_sheet_demo.dart — peek / half / full states |
cd example && flutter run -t lib/demos/glass_modal_sheet_demo.dart |
glass_bottom_bar_demo.dart — magic-lens masking |
cd example && flutter run -t lib/demos/glass_bottom_bar_demo.dart |
bottom_bar_tab_width_demo.dart — tabWidth showcase |
cd example && flutter run -t lib/demos/bottom_bar_tab_width_demo.dart |
searchable_bar_demo.dart — searchable bar edge cases |
cd example && flutter run -t lib/demos/searchable_bar_demo.dart |
shape_debug_demo.dart — GlassButton shapes |
cd example && flutter run -t lib/demos/shape_debug_demo.dart |
quality_comparison_demo.dart — premium & standard quality comparison playground |
cd example && flutter run -t lib/demos/quality_comparison_demo.dart |
Widget Categories #
Containers #
GlassCard · GlassPanel · GlassContainer · GlassDivider · GlassListTile · GlassStepper · GlassWizard
Interactive #
GlassButton · GlassIconButton · GlassChip · GlassSwitch · GlassSlider · GlassSegmentedControl · GlassPullDownButton · GlassButtonGroup · GlassBadge
Input #
GlassTextField · GlassTextArea · GlassPasswordField · GlassSearchBar · GlassPicker · GlassFormField
Feedback #
GlassProgressIndicator · GlassToast · GlassSnackBar
Overlays #
GlassDialog · GlassSheet · GlassModalSheet · showGlassActionSheet · GlassMenu · GlassMenuItem
Surfaces #
GlassAppBar · GlassBottomBar · GlassSearchableBottomBar · GlassTabBar · GlassSideBar · GlassToolbar
Installation #
dependencies:
liquid_glass_widgets: ^0.12.1
flutter pub get
Quick Start #
Two steps — that's the entire setup:
Step 1. Call initialize() in main() to pre-warm shaders.
Step 2. Wrap your app with LiquidGlassWidgets.wrap():
import 'package:flutter/material.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await LiquidGlassWidgets.initialize();
runApp(LiquidGlassWidgets.wrap(child: const MyApp()));
}
That's it. Then add any glass widget to your tree — no per-widget configuration needed:
Scaffold(
appBar: GlassAppBar(title: const Text('My App')),
body: const Center(child: GlassCard(child: Text('Hello, Glass!'))),
)
Accessibility is on by default. The library automatically reads the device's Reduce Motion and Reduce Transparency settings — no extra setup required. See Accessibility for details.
Optional: quality & theming #
For production apps, pass adaptiveQuality and/or theme to wrap() at the same call site:
runApp(LiquidGlassWidgets.wrap(
child: const MyApp(),
adaptiveQuality: true, // auto-benchmarks device, degrades gracefully
theme: GlassThemeData.simple( // optional app-wide glass defaults
blur: 10,
thickness: 30,
quality: GlassQuality.standard,
),
));
Both parameters are optional — omit them and the library uses sensible defaults.
Theming #
Pass a theme: to LiquidGlassWidgets.wrap() to set your app-wide defaults — every glass widget inherits them automatically, no per-widget configuration needed:
runApp(LiquidGlassWidgets.wrap(
child: const MyApp(),
theme: GlassThemeData(
light: GlassThemeVariant(
settings: GlassThemeSettings(thickness: 30, blur: 6),
quality: GlassQuality.standard,
),
dark: GlassThemeVariant(
settings: GlassThemeSettings(thickness: 40, blur: 8),
quality: GlassQuality.standard,
),
),
));
For a quick single-quality theme, use the GlassThemeData.simple shorthand:
runApp(LiquidGlassWidgets.wrap(
child: const MyApp(),
theme: GlassThemeData.simple(
blur: 10,
thickness: 30,
quality: GlassQuality.standard,
),
));
GlassThemeSettingsvsLiquidGlassSettings: UseGlassThemeSettingsinsideGlassThemeVariant. It accepts the same parameters but all are nullable — only fields you explicitly set are applied; everything else inherits from each widget's own defaults.LiquidGlassSettingsis the full settings type used on individual widgets.
Three-level override hierarchy (highest wins):
- Widget
settingsparameter — explicit, widget-level override GlassPage(themeOverride: ...)— per-screen override for special pages (onboarding, paywalls)GlassTheme/wrap(theme:...)— app-wide defaults
Access the current theme programmatically:
final variant = GlassThemeData.of(context).variantFor(context);
Per-subtree theming
For advanced use cases where you need different glass styles within a single screen, place a GlassTheme widget anywhere in your tree:
GlassTheme(
data: GlassThemeData.simple(blur: 4, quality: GlassQuality.minimal),
child: MyListSection(), // list cards get minimal quality
)
Glow Colors #
GlassGlowColors controls the interaction glow emitted by surfaces like GlassBottomBar and GlassSearchableBottomBar:
GlassThemeVariant(
glowColors: GlassGlowColors(
primary: Colors.blue,
glowBlurRadius: 12,
glowSpreadRadius: 0.2,
glowOpacity: 0.8,
),
)
Platform Support #
| Platform | Renderer | Notes |
|---|---|---|
| iOS | Impeller (Metal) | Full shader pipeline, chromatic aberration |
| Android | Impeller (Vulkan) | Full shader pipeline, chromatic aberration |
| macOS | Impeller (Metal) | Full shader pipeline, chromatic aberration |
| Web | CanvasKit | Lightweight fragment shader |
| Windows | Skia | Lightweight fragment shader |
| Linux | Skia | Lightweight fragment shader |
Platform detection is automatic — no configuration required.
Glass Quality Modes #
Standard — Default, Recommended #
The right choice for 95% of use cases. Works on every platform with iOS 26-accurate glass effects.
GlassContainer(
quality: GlassQuality.standard, // this is the default
child: const Text('Great for scrollable content'),
)
Premium — Impeller Only #
Enables the full Impeller shader pipeline with texture capture and chromatic aberration. On Skia/Web, automatically falls back to Standard.
GlassAppBar(
quality: GlassQuality.premium,
title: const Text('Static header'),
)
Use Premium only for static, non-scrolling surfaces (app bars, bottom bars, hero sections). It may not render correctly inside
ListVieworCustomScrollViewon Impeller.
Minimal — Shader-Free #
Zero custom fragment shader cost on any device. Uses BackdropFilter blur + a Rec. 709 saturation matrix + a specular rim stroke. Visually equivalent to a high-quality frosted panel.
GlassCard(
quality: GlassQuality.minimal,
child: const Text('No shader overhead'),
)
Two ideal use cases:
- Device fallback — very old Android devices or any device where
ImageFilter.isShaderFilterSupportedisfalse - GPU budget management — use
minimalfor background panels and list cards while keepingstandardorpremiumon the focal element. A screen with 15 glass list cards runningminimalfires zero shader invocations during scroll
Theme shorthand:
GlassThemeVariant.minimalappliesminimalquality globally viaGlassThemeData.
GlassPage #
GlassPage is the recommended root widget for any screen that uses glass surfaces. It eliminates several common setup mistakes in one widget:
// Minimum — just wrap your Scaffold, GlassPage handles everything else:
GlassPage(
child: Scaffold(
appBar: GlassAppBar(title: const Text('Home')),
body: MyContent(),
),
)
// With a wallpaper:
GlassPage(
background: Image.asset('assets/wallpaper.jpg', fit: BoxFit.cover),
edgeToEdge: true,
statusBarStyle: GlassStatusBarStyle.auto,
child: Scaffold(
appBar: GlassAppBar(title: const Text('Home')),
body: MyContent(),
),
)
| What it handles | Without GlassPage |
|---|---|
Transparent Scaffold |
Must set scaffoldBackgroundColor: transparent manually |
| Navigation ghosting | Must remember to add GlassBackdropScope to every route |
| Background scope setup | Must wrap in LiquidGlassScope manually |
| Status bar icons | Must call SystemChrome.setSystemUIOverlayStyle and restore it |
| Edge-to-edge mode | Must call SystemChrome.setEnabledSystemUIMode and restore it |
| Per-screen theme | Must wrap subtree in a local GlassTheme manually |
Parameters #
| Parameter | Default | Purpose |
|---|---|---|
background |
null |
Optional wallpaper/background widget. When omitted, Scaffold background is left unchanged |
child |
required | Screen content, typically a Scaffold |
enableBackgroundSampling |
true when background is set, false otherwise |
GPU texture capture for real colour absorption. Set false explicitly to opt out |
statusBarStyle |
GlassStatusBarStyle.none |
Status bar icon brightness; auto is recommended for wallpaper screens |
edgeToEdge |
false |
Draw content behind system bars (full immersive) |
themeOverride |
null |
Per-screen GlassThemeData override for special screens |
Tip for
edgeToEdgeon Android: Whentrue, content draws underneath the Android navigation bar. Remember to wrap yourScaffoldbody in aSafeArea(or useextendBody: trueand pad the bottom) so your content isn't hidden behind the system buttons.
Specular Sharpness #
Control the tightness of the specular highlight on any glass surface via LiquidGlassSettings.specularSharpness:
GlassCard(
settings: LiquidGlassSettings(
specularSharpness: GlassSpecularSharpness.sharp, // tight, mirror-like
),
child: ...,
)
| Value | Look |
|---|---|
GlassSpecularSharpness.soft |
Wide, diffuse — frosted / matte glass |
GlassSpecularSharpness.medium |
Default — matches iOS 26 |
GlassSpecularSharpness.sharp |
Tight, polished — mirror-like surface |
Each value maps to a fixed power-of-2 exponent. The GPU uses a zero-transcendental multiply chain for each — no pow() overhead.
Performance Tips #
LiquidGlassWidgets.initialize()at startup — pre-caches shaders, eliminates the white flash on first renderLiquidGlassWidgets.wrap()inmain.dart— installs root backdrop sharing, accessibility, and global theming; passadaptiveQuality: truefor automatic per-device quality tuning. For multi-screen apps, also addGlassBackdropScopeto each route — see Backdrop Isolation- Standard quality for scrollable content — lists, forms, interactive widgets
- Premium quality for fixed surfaces — app bars, bottom bars, and hero sections
- Minimal quality for shader-dense screens — use
GlassQuality.minimalfor background panels and list cards to fire zero custom shader invocations during scroll, then keepstandardorpremiumonly on the focal element - Accessibility fallbacks are zero-cost — when Reduce Transparency is active, the glass shader is bypassed entirely;
BackdropFilterblur runs in Flutter's own paint layer with no custom shader overhead
Automatic Quality Adaptation (experimental) #
📊 Help us tune the thresholds — takes 2 minutes #
GlassAdaptiveScopeis@experimentalbecause its Phase 2 timing thresholds are based on limited community data, not yet validated across the full Android device landscape. Current defaults (v0.12.0):
P75 warmup Quality assigned < 20 ms premium(based on 1 report — please share yours)20–28 ms standard(provisional — no real-device data yet)> 28 ms minimalIf you use
adaptiveQuality: true, please post your results to our Threshold Calibration Discussion with the snippet below. Every report directly informs the threshold calibration and gets us closer to removing@experimental. Thank you 🙏// Add to your GlassAdaptiveScopeConfig while testing — remove before shipping: // Option A: zero-wiring (recommended for quick reports) GlassAdaptiveScopeConfig( debugLogDiagnostics: true, // prints to console in debug builds only ) // Option B: custom handler for analytics GlassAdaptiveScopeConfig( onDiagnostic: (d) { // d.reason, d.p75Ms, d.p95Ms, d.framesMeasured, d.phase are all set debugPrint('📊 ${d.from.name} → ${d.to.name} | reason: ${d.reason.name} | P75: ${d.p75Ms?.toStringAsFixed(1)}ms'); }, )
GlassAdaptiveScope (enabled via wrap(adaptiveQuality: true)) automatically
benchmarks the device at startup and adjusts quality in real time:
// Minimal — let the library decide the best quality for the device:
runApp(LiquidGlassWidgets.wrap(child: const MyApp(), adaptiveQuality: true));
// Per-screen — fine-grained control on specific routes:
GlassAdaptiveScope(
initialQuality: GlassQuality.standard, // conservative start
allowStepUp: true,
// Android calibration — raise if your device is incorrectly demoted to standard.
// Post your P75 + device model to the Threshold Calibration Discussion!
// warmupPremiumThresholdMs: 24.0, // default 20.0
// warmupStandardThresholdMs: 32.0, // default 28.0
child: Scaffold(...),
)
Eliminating repeat warmup jank (recommended for production)
On the first launch, GlassAdaptiveScope runs a ~3-second warm-up benchmark
to measure real raster performance. On a Pixel 4a, this benchmark observes slow
frames and steps down to minimal. Without persistence, this happens on every
cold start — the user sees 3 seconds of degraded quality every time they open
the app.
Within a single app process, the library caches the settled quality automatically. If the scope is disposed and remounted (e.g. navigating away and back to the root), Phase 2 is not re-run — no extra code required.
Across cold starts, use onQualityChanged + initialQuality with your
preferred storage mechanism:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Load previously settled quality — avoids warmup jank on repeat launches.
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('glass_quality');
final initial = saved != null
? GlassQuality.values.byName(saved) // Dart 2.15+ built-in
: null; // null = run Phase 2 on first launch, then persist
await LiquidGlassWidgets.initialize();
runApp(LiquidGlassWidgets.wrap(
child: const MyApp(),
adaptiveQuality: true,
adaptiveConfig: GlassAdaptiveScopeConfig(
initialQuality: initial, // restore immediately — no warmup window
allowStepUp: true, // allow recovery after thermal throttle
onQualityChanged: (_, to) => // persist whenever quality settles
prefs.setString('glass_quality', to.name),
),
));
}
On first launch: initial is null → Phase 2 runs → quality settles → persisted.
On every subsequent launch: initial is non-null → Phase 2 skipped → no jank.
GPU Budget Monitoring #
GlassPerformanceMonitor watches raster frame durations while GlassQuality.premium surfaces are active. When frames exceed the GPU budget for 60 consecutive frames it emits a single FlutterError with actionable guidance — which widget to change, which quality tier to try, and why.
Zero production overhead — automatically disabled in release builds. Enabled by default in debug/profile via LiquidGlassWidgets.initialize():
// Default — auto-enabled in debug/profile, zero-cost in release
await LiquidGlassWidgets.initialize();
// Opt out entirely
await LiquidGlassWidgets.initialize(enablePerformanceMonitor: false);
// Custom thresholds
GlassPerformanceMonitor.rasterBudget = const Duration(microseconds: 8333); // 120 fps
GlassPerformanceMonitor.sustainedFrameThreshold = 120;
Backdrop Isolation — Preventing Ghost Artifacts #
LiquidGlassWidgets.wrap() installs one root BackdropGroup that all glass
surfaces share for GPU backdrop captures. When navigating between screens, the
previous screen's backdrop texture stays bound for 1–2 frames — causing the old
page's content to briefly bleed through glass on the new screen.
Recommended fix: use GlassPage — it handles backdrop isolation automatically.
class MyNewPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// GlassPage creates a fresh GlassBackdropScope on mount automatically.
// No extra widgets needed.
return GlassPage(
child: Scaffold(
appBar: GlassAppBar(title: const Text('New Page')),
body: ...,
bottomNavigationBar: GlassBottomBar(...),
),
);
}
}
Manual alternative — GlassBackdropScope:
If you are not using GlassPage (e.g., you have a custom routing setup), add a
GlassBackdropScope at the root of each route yourself:
class MyLegacyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GlassBackdropScope( // ← forces a fresh capture on mount
child: Scaffold(
appBar: GlassAppBar(title: const Text('New Page')),
body: ...,
),
);
}
}
GlassBackdropScope creates a child BackdropGroup scoped to that screen. The
moment it mounts, it captures a fresh GPU backdrop — no memory of the previous
page's content.
Rule of thumb:
GlassPageis the easiest path — it wrapsGlassBackdropScopefor you. Use rawGlassBackdropScopeonly whenGlassPageis not suitable.
Why adaptiveQuality tabs don't ghost:
When switching tabs within the same screen, all glass surfaces share the same
GlassBackdropScope which is refreshed correctly during animation.
The ghost appears only when crossing a Navigator route boundary or an
AnimatedSwitcher that replaces the whole screen widget.
Custom Refraction for Interactive Indicators #
On Skia and Web, interactive widgets like GlassSegmentedControl can display
true liquid glass refraction from a background image.
Recommended: use GlassPage(background:...) — it wires up the refraction source
automatically and is the cleanest integration path:
// GlassPage handles LiquidGlassScope + GlassRefractionSource for you:
GlassPage(
background: Image.asset('assets/wallpaper.jpg', fit: BoxFit.cover),
child: Scaffold(
body: Center(
child: GlassSegmentedControl(
segments: const ['Option A', 'Option B', 'Option C'],
selectedIndex: 0,
onSegmentSelected: (i) {},
quality: GlassQuality.standard,
),
),
),
)
Manual alternative — LiquidGlassScope:
For advanced scenarios (e.g. isolated sections within a screen, non-GlassPage setups),
use LiquidGlassScope directly:
// Shorthand — wallpaper behind your Scaffold:
LiquidGlassScope.stack(
background: Image.asset('assets/wallpaper.jpg', fit: BoxFit.cover),
content: Scaffold(
body: Center(child: GlassSegmentedControl(...)),
),
)
// Manual — granular control over which surface is sampled:
LiquidGlassScope(
child: Stack(
children: [
Positioned.fill(
child: GlassRefractionSource(
child: Image.asset('assets/wallpaper.jpg'),
),
),
Center(child: GlassSegmentedControl(...)),
],
),
)
On Impeller, GlassQuality.premium uses the native scene graph — no
LiquidGlassScope needed.
Migration note (0.7.0):
LiquidGlassBackgroundwas renamed toGlassRefractionSource. The old name still compiles (deprecated typedef) and will be removed in 1.0.0.
| When | Recommendation |
|---|---|
| Skia / Web (recommended) | GlassPage(background:...) — automatic wiring |
| Skia / Web (manual) | LiquidGlassScope.stack with GlassQuality.standard |
| iOS / macOS (Impeller) | GlassQuality.premium — native scene graph |
| Multiple isolated sections | Separate LiquidGlassScope per section |
Gyroscope Lighting #
GlassMotionScope drives the specular highlight angle from any Stream<double>, including a device gyroscope via sensors_plus:
GlassMotionScope(
stream: gyroscopeEvents.map((e) => e.y * 0.5),
child: Scaffold(
appBar: GlassAppBar(title: const Text('My App')),
body: ...,
),
)
No new dependencies required — connect any stream source (scroll position, mouse, gyroscope).
Accessibility #
Every glass widget in this package respects the user's system accessibility preferences automatically — no setup required.
| System Setting | Effect on glass widgets |
|---|---|
| Reduce Motion (iOS/macOS/Android) | All spring/jelly animations snap instantly to their target |
| Reduce Transparency / High Contrast | Glass shader replaced with a plain frosted BackdropFilter panel — zero GPU shader cost |
No setup needed #
Just ship your app. If the user has Reduce Motion on, your widgets snap. If they have Reduce Transparency on, they get a solid frosted fallback. Nothing to configure.
Optional: GlassAccessibilityScope #
Place GlassAccessibilityScope in your tree to override system defaults — useful for testing, showcases, or per-subtree customisation:
// In your app (optional — place inside MaterialApp.builder for full coverage)
MaterialApp(
builder: (context, child) => GlassAccessibilityScope(
child: child!, // reads system flags automatically
),
)
// Force a specific state (e.g. demo frosted fallback in a settings screen)
GlassAccessibilityScope(
reduceTransparency: true,
child: GlassSettingsPreview(),
)
GlassAccessibilityScope always wins over the system flag — it's the highest-priority override.
Opting out globally #
For experiences where full glass fidelity is intentional (games, creative tools):
// 0.10.0+: child is a required named parameter
runApp(LiquidGlassWidgets.wrap(
child: const MyApp(),
respectSystemAccessibility: false,
));
This disables only the automatic system-flag bridge. An explicit GlassAccessibilityScope in the widget tree still works regardless.
Priority order (highest wins) #
GlassAccessibilityScopein the widget tree — explicit developer override- System
MediaQueryflags — automatic, respects user's OS setting wrap(respectSystemAccessibility: false)— disables (2) globally
Architecture #
Rendering pipeline #
On Impeller, every GlassQuality.premium surface uses a two-pass pipeline:
- Blur pass —
BackdropFilterLayer(ImageFilter.blur), clipped to the exact widget shape. Shared across all surfaces inside aGlassBackdropScope(injected automatically byLiquidGlassWidgets.wrap()). - Shader pass —
BackdropFilterLayer(ImageFilter.shader)— refraction, edge lighting, glass tint, and chromatic aberration.
On Skia/Web, lightweight_glass.frag runs as a single pass with no backdrop capture.
Liquid Morph Engine #
A standalone physics and animation system powering iOS 26-style teardrop morphing. It lives in lib/engine/ and is fully decoupled from any specific widget — GlassMenu is its first consumer.
Key types: GlassMorphController · LiquidMorphState · LiquidMorphPhysics · MorphPhase · MorphSpeed
See docs/LIQUID_MORPH_ENGINE.md for a full integration guide.
Content-Adaptive Glass Strength (0.7.0) #
Both render paths automatically adapt glass strength to background brightness:
- Dark backgrounds → richer, more opaque glass (1.2× strength, brighter Fresnel rim)
- Light backgrounds → subtler, more translucent glass (0.8× strength)
On Impeller, backdrop luminance is sampled directly from the refracted texture (zero extra reads).
On Skia/Web, MediaQuery.platformBrightnessOf provides a lightweight proxy.
Testing #
# All tests
flutter test
# Exclude golden tests
flutter test --exclude-tags golden
# macOS golden tests (require Impeller)
flutter test --tags golden
Dependencies #
Zero third-party runtime dependencies beyond the Flutter SDK.
The glass rendering pipeline builds on the open-source work of whynotmake-it. Their liquid_glass_renderer (MIT) has been vendored and extended with bug fixes, performance improvements, and shader optimisations.
Contributing #
Contributions are welcome. For major changes, open an issue first to discuss your proposal.
License #
MIT — see the LICENSE file for details.
Credits #
Special thanks to the whynotmake-it team for their liquid_glass_renderer (MIT), whose shader pipeline, texture capture, and chromatic aberration work forms the foundation of the rendering engine in this library.