Liquid Glass in Flutter
recently published an update to my Flutter app with Liquid Glass... sort of. Except it's not actually Liquid Glass, it's a Flutter imitation. I've been trying to add Liquid Glass to my Bluesky client Boost Blue since the initial launch of iOS 26. I'm fairly happy with the result, but it's been a major challenge for me. I figured I'd do a write-up on that process in case it's helpful to any other Flutter developers.
Why bother?
I want to head off this question. As I've been following any LG-related announcements in Flutter communities, this question is always there. Liquid Glass is controversial and has its haters (including me!). Not every app needs to support LG. Mine however, absolutely does. An alternative client to a main app bases its value proposition on a superior user experience, and to a lot of users, LG is a superior user experience. LG was one of my most requested features. Third-party clients face a lot of competition, and this was an area that my (friendly) native competitors were beating me on.
Those exact motivations probably don't apply to you. Others might. I'm not here to tell you to use Liquid Glass. But if you think it's right for your app, this should be helpful.
Native Liquid Glass
My initial attempts at implementing Liquid Glass into Boost Blue all tried utilizing actual native Liquid Glass elements, bridged by Platform Views. Within a few months of the iOS 26 release, several packages utilizing this approach were published to pub.dev. I tried multiple.
There were differences in ease of implementation and how well they worked with my current page structures, but ultimately none of them passed basic viability tests. When implemented on a fairly basic page, they worked fine. But Boost's main experiences are scroll views of social media posts. But in a repeating scroll view, these packages were catastrophic for scroll performance. This wasn't just a loss of smooth scrolling. It was completely unusable at times.
I spent a lot of time trying to fix this issue. I won't detail every attempt, but in the final attempt that caused me to give up, I built a simple app with a basic list view of 100 elements, each containing 4 lines of lorem ipsum text. Even in that basic layout, the scroll performance was not usable (although admittedly better than the full app). My theory is that because Flutter app and the LG effect render in different engines, something breaks down between them.
Flutter Liquid Glass
As an alternative to the native approach, other packages attempt to replicate the look of Liquid Glass purely in Flutter. I prototyped several of these into Boost Blue, and eventually landed on liquid_glass_widgets. The default look is okay, but the package exposes a settings parameter, allowing you to refine those defaults. I spent a lot of time tweaking those values, and I think the result passes as native LG to the vast majority of users (which my users have validated).
Those settings:
final glassSettings = LiquidGlassSettings(
thickness: 10,
blur: 2,
refractiveIndex: 4,
glassColor: backgroundColor.withOpacity(0.8),
);General usage:
Scaffold(
body: Stack(
children: [
// Layer 1: Page content, full-bleed
BoostScaffoldData(
topContentPadding: topPadding,
child: child,
),
// Layer 2: Gradient fade from background color → transparent
Positioned(
top: 0, left: 0, right: 0,
height: ThemeManager.scrollFadeExtraHeight + bottomHeight,
child: IgnorePointer(
child: Container(
decoration: ThemeManager.instance.scrollEdgeFadeDecoration(),
),
),
),
// Layer 3: Glass top bar overlay
LiquidGlassTopBar(
title: title,
titleOpacity: titleOpacity,
showBackButton: true,
actions: [...],
),
],
),
bottomNavigationBar: LiquidGlassBottomNavBar(
selectedIndex: _selectedIndex,
tabs: const [
LiquidGlassTab(label: 'Home', icon: Icons.home),
LiquidGlassTab(label: 'Search', icon: Icons.search),
LiquidGlassTab(label: 'Profile', icon: Icons.person),
],
onTabSelected: (index) => setState(() => _selectedIndex = index),
),
); Top bar implementation:
class LiquidGlassTopBar extends StatelessWidget implements ObstructingPreferredSizeWidget {
static const double height = 44.0;
final String? title;
final Widget? titleWidget;
final double titleOpacity;
final VoidCallback? onBackPressed;
final bool showBackButton;
final List<Widget>? actions;
final double actionsOpacity;
const LiquidGlassTopBar({
super.key,
this.title,
this.titleWidget,
this.titleOpacity = 1.0,
this.onBackPressed,
this.showBackButton = true,
this.actions,
this.actionsOpacity = 1.0,
});
@override
Size get preferredSize => const Size.fromHeight(height);
@override
bool shouldFullyObstruct(BuildContext context) => false;
@override
Widget build(BuildContext context) {
return Container(
height: height + MediaQuery.of(context).padding.top,
color: Colors.transparent,
child: SafeArea(
bottom: false,
child: Stack(
children: [
if (titleWidget == null && title != null)
Center(
child: Opacity(
opacity: titleOpacity,
child: Text(
title!,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: ThemeManager.instance.textColor,
),
),
),
),
if (titleWidget != null)
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Center(
child: GlassContainer(
height: 45,
quality: GlassQuality.premium,
useOwnLayer: true,
settings: ThemeManager.instance.liquidGlassSettings,
shape: LiquidRoundedRectangle(borderRadius: 22.5),
child: Padding(
padding: const EdgeInsets.only(left: 4.0, right: 16.0),
child: titleWidget,
),
),
),
),
if (actions != null && actions!.isNotEmpty)
Positioned(
right: 16,
top: 0,
bottom: 0,
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: actions!,
),
),
),
],
),
),
);
}
}Bottom Nav Bar implementation:
class LiquidGlassBottomNavBar extends StatelessWidget {
final int selectedIndex;
final List<LiquidGlassTab> tabs;
final ValueChanged<int> onTabSelected;
final double opacity;
const LiquidGlassBottomNavBar({
super.key,
required this.selectedIndex,
required this.tabs,
required this.onTabSelected,
this.opacity = 1.0,
});
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
// App specific UI
//<>
// Glass bar
GlassBottomBar(
selectedIndex: selectedIndex,
selectedIconColor: Theme.of(context).colorScheme.primary,
unselectedIconColor: Theme.of(context).colorScheme.onSurface,
settings: glassSettings,
indicatorColor: Colors.white.withValues(alpha: 0.1),
onTabSelected: onTabSelected,
tabs: tabs.map((t) => GlassBottomBarTab(
label: t.label,
icon: Icon(t.icon),
)).toList(),
),
],
);
}
}
class LiquidGlassTab {
final String label;
final IconData icon;
const LiquidGlassTab({required this.label, required this.icon});
}Result
Highlights to show: The custom top nav section of the Home page, and the bottom Tab Bar. This effect isn't identical to the native one, but it's pretty close. Content behind the glass is blurred and refracted around the edges. Scrolling is smooth. And a side benefit: it's available for Android, which has been a surprisingly popular option.
Conclusion
None of this is technically complex. The real challenge was tuning the settings until the look was right. Hopefully this post serves as a shortcut for anyone wanting to do the same.
I'm planning to stick with this long term. I don't expect the native bridge approach to become viable for scroll heavy apps any time soon, and there's no indication that first-party Flutter support is on the Flutter team's roadmap. For now, this is the approach I'd recommend to anyone who needs Liquid Glass in a Flutter app.
Happy to answer questions if you're attempting something similar. You can find me on Bluesky, or see the result yourself in Boost Blue.
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.