The bottom navigation bar is the single most-customized widget in any Flutter app and also the one most developers copy-paste from the wrong Medium post. This guide collects 12 production patterns that actually work in 2026, wires each one with go_router ShellRoute so state persists across tabs, and shows which variant to pick for which app style. Every example includes the full Dart code. No dependencies beyond Flutter 3.29 and Material 3.
Short version: start with Material 3 NavigationBar if you want native feel. Swap to a pill bar if you want floating indie-app aesthetics. Use a curved concave or floating dock if your brand demands a signature look. All 12 examples below work the same way underneath: aStatefulShellRoute.indexedStack persists each tab's navigation stack, your custom widget just renders the bar above the shell content.
The shared shell pattern (wire once, style 12 ways)
Every example below assumes the same go_router scaffold. The difference between the 12 patterns is the bottomNavigationBar widget you render. The shell code stays identical.
class HomeScaffold extends StatelessWidget {
const HomeScaffold({super.key, required this.shell});
final StatefulNavigationShell shell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: shell,
bottomNavigationBar: MyBottomNav(
selectedIndex: shell.currentIndex,
onSelect: (i) => shell.goBranch(i, initialLocation: i == shell.currentIndex),
),
);
}
}shell.currentIndex is the source of truth for which tab is active.shell.goBranch switches tabs while preserving each tab's navigation state. TheinitialLocation parameter re-roots the tab when the user taps a tab they're already on.
The three design families in 2026
Twelve variants fall into three design families. Pick your family first; individual variants are style tweaks within the family.
| Family | Feel | Best for | Examples (below) |
|---|---|---|---|
| Flat bar (full-width) | Native Material 3, Google apps | Productivity, SaaS, finance | 1, 2, 3, 4 |
| Floating / pill | Indie apps, modern consumer | Social, habit trackers, lifestyle | 5, 6, 7, 8 |
| Shaped / signature | Distinctive brand | Consumer apps with design budget | 9, 10, 11, 12 |
1. Material 3 NavigationBar (the default and often the right answer)
Flutter's stock NavigationBar handles accessibility, theming, haptics, and indicator animation automatically. Use this unless you have a specific brand reason not to.
NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: onSelect,
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.explore_outlined), selectedIcon: Icon(Icons.explore), label: 'Explore'),
NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'You'),
],
)2. Material 3 with badge (notifications count)
Same as above but with a Badge wrapped around the icon. Update the badge count via a BLoC or Riverpod listener.
NavigationDestination(
icon: Badge.count(count: unreadCount, child: const Icon(Icons.notifications_outlined)),
selectedIcon: const Icon(Icons.notifications),
label: 'Inbox',
)3. Compact NavigationBar (for small screens)
Reduce height and hide labels for a denser bar on small-screen devices. DroplabelBehavior to onlyShowSelected.
NavigationBar(
height: 64,
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
selectedIndex: selectedIndex,
onDestinationSelected: onSelect,
destinations: destinations,
)4. Elevated material with shadow (subtle lift)
Wrap the bar in a Material with elevation to lift it off the content. Good for apps where the bar should feel distinct from scrolling content.
Material(
elevation: 8,
shadowColor: Colors.black.withOpacity(0.1),
child: NavigationBar(...),
)5. Floating pill bar (the indie-app favorite)
A pill-shaped bar that floats above the content with margin on all sides. The signature look of 2024-2026 indie apps.
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Container(
height: 68,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(34),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 20, offset: const Offset(0, 8)),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(destinations.length, (i) {
final selected = i == selectedIndex;
return GestureDetector(
onTap: () => onSelect(i),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 220),
padding: EdgeInsets.symmetric(horizontal: selected ? 16 : 12, vertical: 10),
decoration: BoxDecoration(
color: selected ? Theme.of(context).colorScheme.primaryContainer : Colors.transparent,
borderRadius: BorderRadius.circular(22),
),
child: Row(children: [
Icon(destinations[i].icon, size: 22),
if (selected) Padding(padding: const EdgeInsets.only(left: 8), child: Text(destinations[i].label)),
]),
),
);
}),
),
),
)The key trick is the AnimatedContainer with selected/unselected padding. Selected tabs expand to show the label, unselected tabs shrink to icon-only. Smooth 220ms transition.
6. Floating pill with glassmorphism
Same shape as #5 but with a BackdropFilter for a Liquid Glass effect. Pair with the iOS 26 design direction from our Liquid Glass guide.
7. Floating center dock (signature FAB-style)
A pill bar with a raised center button that overflows the bar. Perfect for apps with a primary create action (Instagram new post, TikTok record).
8. Animated indicator bar
A slim bar under the selected tab that animates horizontally between positions. UseAnimatedPositioned inside a Stack.
9. Curved concave (the BottomAppBar with FAB notch)
The classic BottomAppBar with a concave notch for a centered FAB. Material 3 still ships this and it still works for apps with a create-first primary action.
Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
shape: const CircularNotchedRectangle(),
notchMargin: 8,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(icon: const Icon(Icons.home), onPressed: () => onSelect(0)),
IconButton(icon: const Icon(Icons.explore), onPressed: () => onSelect(1)),
const SizedBox(width: 48),
IconButton(icon: const Icon(Icons.chat), onPressed: () => onSelect(2)),
IconButton(icon: const Icon(Icons.person), onPressed: () => onSelect(3)),
],
),
),
)10. Curved convex (the opposite of #9, bar arcs over the content)
A ClipPath with a custom Path that arcs downward. Less common but memorable. Watch for safe-area bleed on newer iPhones.
11. Tab strip (segmented control style)
An iOS-ish segmented bar where the selected tab has a filled background that slides between positions. Good for apps with 2 or 3 tabs, poor fit for 4+.
12. Gesture-responsive bar (scroll-to-hide)
A bar that slides off-screen as the user scrolls down and reappears on scroll-up. Increases content area on small screens. Wire with a ScrollController listener plus anAnimatedPositioned.
class ScrollHidingBar extends StatefulWidget {
// ... boilerplate
@override
State<ScrollHidingBar> createState() => _ScrollHidingBarState();
}
class _ScrollHidingBarState extends State<ScrollHidingBar> {
bool _visible = true;
double _lastOffset = 0;
void _onScroll(ScrollNotification n) {
if (n is ScrollUpdateNotification) {
final delta = n.metrics.pixels - _lastOffset;
if (delta > 4 && _visible) setState(() => _visible = false);
if (delta < -4 && !_visible) setState(() => _visible = true);
_lastOffset = n.metrics.pixels;
}
}
// Wrap your Scaffold body in NotificationListener<ScrollNotification>(onNotification: _onScroll, ...)
// and wrap your bottom nav in AnimatedContainer(height: _visible ? 82 : 0, ...)
}Performance notes
- Keep any blur effect (Liquid Glass, glassmorphism) clipped to the bar's own bounds to avoid re-rendering the screen behind.
- Use
RepaintBoundaryaround animated parts so rebuild costs stay local. - Avoid wrapping the whole bar in an
AnimatedBuilderthat rebuilds on every frame. Rebuild only the selected tab. - Profile with the Flutter inspector's performance overlay. If the bar costs more than 1 ms per frame, something is wrong.
Accessibility
- Every tap target must be at least 48x48 logical pixels (Material 3 default bar is 56 height, pill bars often cheat low).
- Wrap icons in
Semantics(label: 'Home tab')so screen readers announce correctly. - Test with TalkBack on Android and VoiceOver on iOS before shipping.
- If you animate the selected-tab indicator, respect
MediaQuery.disableAnimationsand skip the transition.
Picking the right variant (decision matrix)
| If your app... | Pick variant |
|---|---|
| Needs to feel native on Android | 1, 2, or 3 (Material 3) |
| Is an indie consumer app with a design budget | 5, 6, or 7 (floating pill) |
| Targets iOS 26 with Liquid Glass | 6 (glassmorphism pill) with platform gate |
| Has a primary create action (new post, record, scan) | 7 (floating center dock) or 9 (curved concave) |
| Is content-dense (social feeds, reading) | 12 (scroll-to-hide) |
| Has 2 or 3 tabs only | 11 (tab strip / segmented) |
| Wants a distinctive brand signature | 10 (curved convex) |
What The Flutter Kit ships
The Flutter Kit ships three of the most popular bottom nav variants pre-wired: Material 3 (default), floating pill (indie-app style), and floating center dock (create-first apps). All three are bound to the go_router StatefulShellRoute shell and adapt automatically to light/dark mode via Material 3 tokens. Swap between them with one line in app_config.dart.
$69 one-time, unlimited commercial projects. See every integration on the features page or jump to checkout.
Final recommendation
If you have five minutes, use Material 3 NavigationBar (variant 1). If you have thirty minutes and care about the indie-app aesthetic, use the floating pill (variant 5). If your app has a signature create action, use the floating center dock (variant 7). Everything else is polish.
All twelve patterns share the same shell wiring. Once that is set up, switching between variants is purely a styling change. Start with the default and iterate.