liquid_glass_floating_nav 0.1.0
liquid_glass_floating_nav: ^0.1.0 copied to clipboard
A premium floating Liquid Glass bottom navigation bar with smooth drag-to-preview, an organic liquid bubble indicator and light/dark support.
liquid_glass_floating_nav #
A premium floating Liquid Glass bottom navigation bar for Flutter: a frosted glass pill that floats above your content, with a single organic liquid bubble that slides behind the active tab, follows your finger on drag, and commits the page change only once you let go.
Pure Flutter (no native code, no third-party dependencies). Works in any Material app on Android, iOS, web, macOS, Windows and Linux.
Demo #

▶️ Watch the full demo on YouTube
Features #
- Floating frosted-glass pill with a real
BackdropFilterblur clipped to the bar only (the whole screen is never blurred; content frosts through). - One shared liquid capsule that slides and stretches between tabs (it flows, it does not teleport); never a per-tab background.
- Tap to navigate: fires immediately with a selection haptic.
- Smooth drag preview: the bubble follows your finger continuously.
- Commit only on release: the page never changes mid-drag; navigation fires exactly once, for the nearest tab. Dragging across tabs never opens the screens in between.
- Organic liquid bubble/drop: a tapered-superellipse "lens" shape (not a rectangle, pill, oval or circle) with gradient, reflection, rim-light border, soft glow and depth shadow.
- Balanced top + bottom overflow: the bubble bulges equally over both edges while dragging (floating through the bar), never clipped, never into the safe area.
- Horizontal liquid stretch: widens into a centre-anchored capsule and elongates toward a flick.
- Menu magnification (liquid lens): items magnify by distance from the bubble centre (the icon leads the label).
- Filled active icons: selected tab and the tab under the bubble while dragging show their filled glyph.
- Light and dark mode: colours auto-resolve from the ambient theme.
SafeAreaaware, max-width centring on tablets, no overflow on small screens,RepaintBoundary-isolated for performance.- Reduce-motion support and per-tab semantic labels.
Installation #
Add the dependency:
dependencies:
liquid_glass_floating_nav: ^0.1.0
Then run flutter pub get and import it:
import 'package:liquid_glass_floating_nav/liquid_glass_floating_nav.dart';
Basic usage #
int _index = 0;
LiquidGlassBottomNav(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: const [
LiquidGlassNavItem(icon: Icons.home_outlined, selectedIcon: Icons.home, label: 'Home'),
LiquidGlassNavItem(icon: Icons.search_outlined, selectedIcon: Icons.search, label: 'Search'),
LiquidGlassNavItem(icon: Icons.person_outline, selectedIcon: Icons.person, label: 'Profile'),
],
);
Complete example (Scaffold + IndexedStack) #
import 'package:flutter/material.dart';
import 'package:liquid_glass_floating_nav/liquid_glass_floating_nav.dart';
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _index = 0;
// Build these ONCE and hold them in State; do not recreate on tab change.
static const _items = <LiquidGlassNavItem>[
LiquidGlassNavItem(icon: Icons.home_outlined, selectedIcon: Icons.home, label: 'Home'),
LiquidGlassNavItem(icon: Icons.grid_view_outlined, selectedIcon: Icons.grid_view, label: 'Shop'),
LiquidGlassNavItem(icon: Icons.shopping_cart_outlined, selectedIcon: Icons.shopping_cart, label: 'Cart', badgeCount: 3),
LiquidGlassNavItem(icon: Icons.receipt_long_outlined, selectedIcon: Icons.receipt_long, label: 'Orders'),
LiquidGlassNavItem(icon: Icons.person_outline, selectedIcon: Icons.person, label: 'Account'),
];
static const _pages = <Widget>[HomePage(), ShopPage(), CartPage(), OrdersPage(), AccountPage()];
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true, // let content frost under the floating pill
body: IndexedStack(index: _index, children: _pages),
bottomNavigationBar: LiquidGlassBottomNav(
currentIndex: _index,
items: _items,
onTap: (i) => setState(() => _index = i),
),
);
}
}
IndexedStack keeps every tab mounted and shows only index, so scroll position
and loaded data survive tab switches. The bar's currentIndex and the
IndexedStack's index are the same variable: one source of truth.
Give every item an outline icon and a filled selectedIcon (e.g.
Icons.home_outlined to Icons.home) so the outlined-to-filled swap reads
cleanly. If you omit selectedIcon, it defaults to icon (the glyph simply does
not change).
How it behaves #
Tap #
Tapping a tab calls onTap(index) immediately (with a selection haptic).
Your setState updates currentIndex and the capsule flows from the old tab
to the new one.
Drag preview #
Press and drag horizontally anywhere on the bar: the liquid capsule detaches and follows your finger continuously (clamped inside the pill), swells, widens into a horizontal capsule, overflows balanced over both edges, and magnifies each tab it passes over.
Page change commits only once, on release #
onTap is never called during the drag: no screen opens and no per-tab
haptic fires. While dragging, the tab under the bubble previews its filled
icon so you can see what you are about to select, but currentIndex is
untouched. On release, the capsule settles onto the nearest tab and onTap
is called exactly once (and only if that tab differs from the current one). A
cancelled gesture settles back with no navigation. This is why dragging Home to
Account previews the in-between tabs without opening them.
Organic liquid bubble #
The indicator is drawn as a tapered superellipse ("liquid lens"): soft, continuously-rounded corners with a fuller, gently-bulged centre tapering to soft rounded tips, so it never reads as a rectangle, flat rounded rectangle, hard pill, pure oval or circle. It carries a brand-tint gradient fill, a specular dome, an inner shade for volume, a top-biased rim-light border, a soft outer glow and a depth shadow.
Balanced top/bottom overflow #
While dragging, the drop expands symmetrically from the bar's vertical centre: its top and bottom edges push out by the same amount, so it straddles the surface and floats through the bar rather than only popping out the top. The overflow paints into transparent headroom reserved above and below the glass (the bottom strip is carved out of the float gap, so the visible bar never moves and the overflow can never reach the safe area). Nothing is clipped.
Horizontal stretch #
A centre-anchored dragWiden pulls the drop into a prominent horizontal capsule
(growing equally left and right), and a velocity-driven dragStretch elongates
it further toward the direction you flick.
Menu magnification #
Each item magnifies under a liquid lens: a Gaussian of its distance from the live capsule centre, so the item under the bubble gets the full magnification, a neighbour a faint share, and far items nothing. The icon swells more than the label.
Filled active icons #
A tab shows its filled selectedIcon when it is the committed selection and,
while dragging, when the bubble is centred over it (the nearest tab to the bubble
centre; it switches at the midpoint between slots, so there is no flicker). The
committed index never changes until release.
Customization #
All customization goes through LiquidGlassNavTheme, passed as theme:. Every
field has a default, so override only what you need:
LiquidGlassBottomNav(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: _items,
theme: const LiquidGlassNavTheme(
accentColor: Color(0xFF6750A4),
height: 76,
maxWidth: 460,
blurSigma: 18,
capsuleGlow: 1.3,
dragWiden: 0.40,
dragTopOverflow: 8,
dragBottomOverflow: 8, // keep equal to dragTopOverflow for a balanced drop
),
)
| Option | Field | Default |
|---|---|---|
| Nav height | height |
72 |
| Max width | maxWidth |
430 |
| Horizontal margin | horizontalMargin |
16 |
| Bottom margin (float gap) | bottomGap |
12 |
| Blur strength | blurSigma |
16 |
| Background opacity | surfaceColor |
auto (brightness-aware) |
| Border opacity | borderColor |
auto (brightness-aware) |
| Selected bubble height | capsuleVerticalInset |
6 |
| Bubble horizontal inset | blobHorizontalInset |
5 |
| Bubble overflow (top / bottom) | dragTopOverflow / dragBottomOverflow |
6 / 6 |
| Reserved bottom headroom | bottomLiftHeadroom |
10 |
| Horizontal stretch (widen) | dragWiden |
0.34 |
| Directional stretch | dragStretch |
0.24 |
| Drag lift/elevation | dragLift |
0 |
| Capsule swell | dragMagnify |
0.11 |
| Glow strength | capsuleGlow |
1.0 |
| Magnification (icon / label) | lensIconGain / lensLabelGain |
0.28 / 0.12 |
| Selected color | accentColor |
auto (colorScheme.primary) |
| Unselected color | unselectedColor |
auto (brightness-aware) |
| Icon / label size | iconSize / labelFontSize |
24 / 11 |
| Inner padding | innerHorizontalPadding |
8 |
| Animation duration | slideDuration |
360ms |
| Animation curve | standardCurve / emphasizedCurve |
easeOutCubic / easeOutBack |
| Press / switch / style durations | pressDuration / iconSwitchDuration / styleDuration |
120 / 200 / 300 ms |
Dark mode #
Light/dark is automatic: leave the colour fields (accentColor, surfaceColor,
borderColor, unselectedColor) null and the bar resolves them from
Theme.of(context) and the current brightness. Provide a MaterialApp theme +
darkTheme and it follows the system. Override any field to force a look.
Performance notes #
- The
BackdropFilterblur is clipped to the bar (a small pill), never the full screen: the single biggest perf win versus a naive full-screen blur. - The whole bar sits in a
RepaintBoundary, so its per-frame repaints do not dirty the rest of the tree. - During slide/drag, only the capsule and the lens transforms repaint; the tappable item row is built once per layout and reused as a static child.
- The drag-physics ticker stops itself when the effect settles, so there is zero idle CPU when you are not interacting.
- Use
flutter run --release/--profileto judge real performance (debug mode is much slower).
Accessibility #
- Each tab exposes
Semantics(button: true, selected: ..., label: ...); providesemanticLabelon aLiquidGlassNavItemto override the spoken label. - When the OS "reduce motion" setting is on (
MediaQuery.disableAnimations), every transition collapses to an instant state change. - Labels are always shown and ellipsise rather than overflow.
Troubleshooting #
| Symptom | Cause and fix |
|---|---|
| Nav appears in the middle of the screen | You put it in body or wrapped it in Center. Pass it to Scaffold.bottomNavigationBar (it uses Align(heightFactor: 1) internally to shrink-wrap). |
| Animation does not slide / it jumps | You are rebuilding the widget fresh each tab change, or changing its key, which throws away the animation controller. Keep one persistent currentIndex in State and change only that. Also check the OS reduce-motion setting. |
| Drag opens every screen it passes | Your onTap handler navigates for intermediate indices. The component calls onTap only once on release; make sure your handler does not push routes during the drag. |
| Drag feels sticky / laggy | A too-high blurSigma on a low-end device, or a heavy tree rebuilding behind it. Lower blurSigma, wrap the page body in its own RepaintBoundary/lazy list, and test in profile/release mode. |
| Bubble clipped from the top or bottom | An ancestor ClipRect/ClipRRect is cutting the reserved headroom. Remove the clip around the bottom nav, or lower dragTopOverflow / dragBottomOverflow. Keep bottomLiftHeadroom < bottomGap. |
| Bubble looks circular | The vertical swell is too large relative to the width. Increase dragWiden, or lower dragTopOverflow / dragBottomOverflow. |
| Bubble looks oval | Defaults already taper it into a lens; if you changed shape knobs, restore dragWiden and the default insets so it stays wider than tall. |
| Bubble looks rectangular | This should not happen with defaults (the shape is a tapered superellipse). Do not wrap the capsule region in a square clip; keep the default blobHorizontalInset. |
| Icons not becoming filled | You did not provide a distinct selectedIcon (it defaults to icon). Pass the filled counterpart, e.g. Icons.home_outlined to Icons.home. |
| Content hidden behind the nav | Set extendBody: true on the Scaffold and add bottom padding (about height + 40) to scrollable content. The bar floats over content by design. |
Example app #
A full runnable example lives in the example/ directory: MaterialApp with
light/dark themes, a stateful shell, five tabs (Home, Shop, Cart, Orders,
Account) with outline + filled icons, an IndexedStack body, extendBody: true,
and colourful scrollable pages so you can see the glass blur. Run it with:
cd example
flutter create . # one-time: generate platform folders (android/ios/...)
flutter run
License #
This package is released under the MIT License. See the LICENSE file.
Copyright (c) 2026 Satyam Sharma satyamsharma923@gmail.com
The implementation is original. It is inspired by the Liquid Glass visual style
and interaction behaviour, but contains no code copied from liquid_glass_bar or
any other package.
Contributing #
Issues and pull requests are welcome. Please run dart format . and
flutter analyze before opening a PR, and add or update tests in test/ where
practical.