liquid_glass_bottom_nav 0.0.14
liquid_glass_bottom_nav: ^0.0.14 copied to clipboard
A beautiful, liquid frosted-glass floating bottom navigation bar for Flutter.
liquid_glass_bottom_nav #
A floating frosted-glass pill bottom navigation bar for Flutter. Features a physics-based sliding bubble indicator, drag-to-switch gestures, and a squash-and-stretch tilt animation. Built on top of liquid_glass_renderer — embedded directly, so no extra dependency is needed.
Automatically falls back to a standard Material bottom nav on devices that don't support Impeller (Skia, web, Windows, Linux).

Installation #
dependencies:
liquid_glass_bottom_nav: ^0.0.11
Try the example app #
example/ is a full interactive playground — tab count, active color, icon size, capsule radius, badge count, legacy-fallback toggle, drag rainbow border, and liquid active bubble are all live-tunable from its Settings tab. Ships with ios/ and android/ platform folders.
cd example
flutter run # auto-picks a device if only one is connected
flutter run -d <device-id> # or pick one from `flutter devices`
No simulator/emulator booted yet? See example/README.md for exact boot commands.
Quick start #
Wrap your Scaffold body in a Stack and place LiquidGlassNavBar as a Positioned overlay at the bottom. Add LiquidGlassNavBar.contentBottomInset as bottom padding on any scrollable content so it clears the nav bar.
import 'package:liquid_glass_bottom_nav/liquid_glass_bottom_nav.dart';
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _selectedIndex = 0;
final _items = const [
LiquidGlassNavItem(id: 'home', icon: Icons.home_rounded, label: 'Home'),
LiquidGlassNavItem(id: 'search', icon: Icons.search_rounded, label: 'Search'),
LiquidGlassNavItem(id: 'profile', icon: Icons.person_rounded, label: 'Profile'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// Your page content — add bottom padding so it clears the nav bar
Positioned.fill(
child: SingleChildScrollView(
padding: const EdgeInsets.only(
bottom: LiquidGlassNavBar.contentBottomInset,
),
child: /* your content */,
),
),
// The nav bar
LiquidGlassNavBar(
items: _items,
selectedIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
),
],
),
);
}
}
Customization #
Colors #
LiquidGlassNavBar(
items: _items,
selectedIndex: _selectedIndex,
onTap: (i) => setState(() => _selectedIndex = i),
activeColor: const Color(0xFF4CAF50), // active icon + label
inactiveColor: Colors.white60, // inactive icon + label
),
Icon size and label style #
LiquidGlassNavBar(
items: _items,
selectedIndex: _selectedIndex,
onTap: (i) => setState(() => _selectedIndex = i),
iconSize: 24,
labelStyle: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
Custom icon widget #
Use iconWidget when you need an SVG, asset image, or any custom painter instead of a IconData. The widget is wrapped in IconTheme so any widget that respects it (including SvgPicture.asset with colorFilter: ColorFilter.mode(iconTheme.color!, BlendMode.srcIn)) will automatically receive the active/inactive color.
LiquidGlassNavItem(
id: 'mosque',
label: 'Prayer',
iconWidget: SvgPicture.asset(
'assets/icons/mosque.svg',
colorFilter: ColorFilter.mode(
IconTheme.of(context).color ?? Colors.white,
BlendMode.srcIn,
),
),
),
Insets #
LiquidGlassNavBar(
items: _items,
selectedIndex: _selectedIndex,
onTap: (i) => setState(() => _selectedIndex = i),
insets: 12.0, // horizontal margin from screen edge + bottom margin above safe area
),
Capsule radius #
borderRadius controls the corner radius of the floating capsule. It defaults to 34.0, which — at the bar's fixed 68px height — renders as a full stadium/pill. Lower it for a squarer capsule; the active tab bubble automatically follows the same radius so its shape always matches the capsule around it.
LiquidGlassNavBar(
items: _items,
selectedIndex: _selectedIndex,
onTap: (i) => setState(() => _selectedIndex = i),
borderRadius: 16.0,
),
Only affects the Impeller-rendered capsule — the legacy fallback bar (used when impellerSupported is false) is a full-width bar with no capsule shape, so borderRadius has no effect there.
Bubble-sheen border while dragging #
While actively dragging, the held capsule (already larger than the resting pill — see the drag mechanics above) loses its flat activeColor tint and its border becomes a soft iridescent "soap bubble" sheen instead — a blurred pastel gradient rim with a glossy highlight. The icon and label themselves are never scaled; only the capsule's own hold-state growth and this border change.
LiquidGlassNavBar(
items: _items,
selectedIndex: _selectedIndex,
onTap: (i) => setState(() => _selectedIndex = i),
dragRainbowBorder: false, // opt out, back to a flat activeColor stroke
),
A plain-widget effect (a static CustomPainter) — no BackdropFilter or shader involved, so it adds no per-frame rendering cost during drag.
Real glass on the held bubble (liquidActiveBubble) #
By default the bubble is always a plain tinted BoxDecoration — cheap, and consistent with the "keep glass shapes static" guidance below. Setting liquidActiveBubble: true swaps it for a real LiquidGlass shape instead, but only while actively held/dragging — the resting (selected-but-not-dragging) bubble always stays the plain tinted container regardless of this flag.
LiquidGlassNavBar(
items: _items,
selectedIndex: _selectedIndex,
onTap: (i) => setState(() => _selectedIndex = i),
liquidActiveBubble: true,
),
This is off by default for a reason. While held, this bubble's position and size animate every drag frame. With liquidActiveBubble: true, the glass shader recomputes (toImageSync()) on every one of those frames — the exact per-frame shader cost this package otherwise avoids everywhere else by keeping all glass shapes static. Test on real mid/low-end Android hardware before shipping with this enabled; it has not been profiled for frame-time impact.
Badge count #
Set badgeCount on a LiquidGlassNavItem to show a small badge on that tab's icon — e.g. for unread notifications. badgeColor, badgeTextColor, and badgeBorderRadius are per-item, so different tabs can have different badge styling. Works in both Impeller and legacy fallback modes.
LiquidGlassNavItem(
id: 'alerts',
icon: Icons.notifications_rounded,
label: 'Alerts',
badgeCount: unreadCount, // null or <= 0 hides the badge; caps display at "99+"
badgeColor: Colors.red, // defaults to colorScheme.error
badgeTextColor: Colors.white, // defaults to colorScheme.onError
badgeBorderRadius: 999, // defaults to 999 (fully rounded); lower for a squarer badge
),
The badge is a pure reflection of badgeCount — the package has no built-in "dismiss on tap" behavior. To clear it, update your own state (e.g. when the user reads their notifications) and pass badgeCount: null or 0 back into the item:
LiquidGlassNavItem(
id: 'alerts',
icon: Icons.notifications_rounded,
label: 'Alerts',
badgeCount: _unreadCount == 0 ? null : _unreadCount,
),
// elsewhere, e.g. when the Alerts tab is opened:
onTap: (index) {
setState(() {
_selectedIndex = index;
if (index == alertsIndex) _unreadCount = 0;
});
},
API reference #
LiquidGlassNavBar #
| Parameter | Type | Default | Description |
|---|---|---|---|
items |
List<LiquidGlassNavItem> |
required | Navigation items |
selectedIndex |
int |
required | Index of the active tab |
onTap |
ValueChanged<int> |
required | Called when a tab is tapped or dragged to |
activeColor |
Color? |
colorScheme.primary |
Icon and label color for the active tab |
inactiveColor |
Color? |
colorScheme.onSurface |
Icon and label color for inactive tabs |
iconSize |
double |
22 |
Size of the icon in each tab |
labelStyle |
TextStyle? |
bodySmall + w500 |
Merged over the default label style. Color is always driven by active/inactive color. |
insets |
double |
16 |
Horizontal margin from screen edge and bottom margin above the system safe area |
borderRadius |
double |
34 |
Corner radius of the floating capsule. The active tab bubble follows the same radius. No effect on the legacy fallback bar. |
dragRainbowBorder |
bool |
true |
When true, the held bubble's border is a pastel gradient sheen instead of a tinted activeColor stroke. |
liquidActiveBubble |
bool |
false |
When true, the bubble is a real LiquidGlass shape while held/dragging (resting state is unaffected). Animates every drag frame — not profiled for frame-time impact, off by default. |
impellerSupported |
bool |
true |
Pass false to force the legacy Material fallback (useful when Impeller is unavailable) |
LiquidGlassNavItem #
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
String |
yes | Unique identifier for this item |
label |
String |
yes | Tab label text |
icon |
IconData? |
one of icon/iconWidget |
Material or Cupertino icon |
iconWidget |
Widget? |
one of icon/iconWidget |
Custom icon widget — takes priority over icon. Wrapped in IconTheme. |
badgeCount |
int? |
no | Notification count shown as a badge on the icon. null or <= 0 hides it; values above 99 display as "99+". |
badgeColor |
Color? |
no | Badge background color. Defaults to colorScheme.error. |
badgeTextColor |
Color? |
no | Badge text color. Defaults to colorScheme.onError. |
badgeBorderRadius |
double |
no (default 999) |
Corner radius of the badge shape. 999 is fully rounded; lower for a squarer badge. |
Static constants #
| Constant | Value | Description |
|---|---|---|
LiquidGlassNavBar.contentBottomInset |
96.0 |
Bottom padding to add to scrollable content so it clears the floating nav bar |
LiquidGlassBackButton #
A floating glass back button that matches the nav bar's visual language. Place it as a top-left overlay inside a Stack.
Stack(
children: [
YourPageContent(),
Positioned(
top: MediaQuery.of(context).padding.top + 12,
left: 16,
child: LiquidGlassBackButton(
onTap: () => Navigator.of(context).pop(),
),
),
],
)
LiquidGlassBackButton #
| Parameter | Type | Default | Description |
|---|---|---|---|
onTap |
VoidCallback? |
Navigator.maybePop |
Tap handler. Defaults to popping the current route. |
color |
Color? |
colorScheme.onSurface |
Chevron icon color |
size |
double |
44 |
Width and height of the glass pill |
borderRadius |
double |
14 |
Corner radius of the glass shape |
impellerSupported |
bool |
true |
Pass false to fall back to a plain IconButton |
Impeller note #
The glass effect requires Flutter's Impeller renderer.
- iOS — Impeller is on by default. No action needed.
- Android — Impeller must be enabled in
AndroidManifest.xml:
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="true" />
When Impeller is unavailable (older Android, web, desktop), the widget automatically renders a standard Material bottom navigation bar. You can also force the fallback by passing impellerSupported: false.
License #
MIT