The bottom tab bar is the backbone of most mobile apps — Home, Search, Profile, one tap apart. Flutter's BottomNavigationBar draws the bar, but switching screens and keeping each tab's state alive is on you. Get that wiring right and tabs feel instant; get it wrong and every switch resets scroll positions and form input. This guide covers the items and selection, switching screens with IndexedStack to preserve state, the fixed-versus-shifting gotcha, and the Material 3 NavigationBar, with paste-ready code.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowEvery snippet below is paste-ready against current stable Flutter. A bottom bar lives in the Scaffold, and switching tabs is a form of navigation — though, importantly, not a route push.
We'll build a three-tab bar, switch screens, preserve state with IndexedStack, fix the fixed-type gotcha, and move to the Material 3 NavigationBar.
If you'd rather watch a tabbed app shell built and themed, the channel walks through real navigation structures.
Items and selection
BottomNavigationBar goes in the Scaffold's bottomNavigationBar slot. It takes a list of BottomNavigationBarItems, a currentIndex for which is selected, and an onTap callback that fires with the tapped index. You store that index in state.
int _index = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_index],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
The bar itself is stateless — it only reports taps. You own the selected index and decide what the body shows for it, which is what makes the next step, preserving state, your responsibility too.
Preserve state with IndexedStack
Showing _screens[_index] directly works, but it rebuilds each screen from scratch on every switch — losing scroll position, text input, and any controllers. Wrap the screens in an IndexedStack, which keeps every child mounted and shows only the active one.
body: IndexedStack(
index: _index,
children: const [HomeScreen(), SearchScreen(), ProfileScreen()],
),
Now each tab keeps its state alive in the background — exactly what users expect when they flick back to a half-scrolled feed. The trade-off is that all tabs build up front; for heavy screens you can lazily construct them, but for most apps IndexedStack is the right default.

Ship a real tabbed app
The Complete Flutter Guide builds full app shells with navigation, state, and Firebase from the ground up.
Enrol nowThe fixed vs shifting gotcha
With three items or fewer the bar is fixed; with four or more it switches to the shifting type, which can hide unselected labels and tint the bar with each item's colour. If that surprises you, set type explicitly.
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.deepPurple,
unselectedItemColor: Colors.grey,
// ...items, currentIndex, onTap
)
BottomNavigationBarType.fixed keeps every label visible and one colour scheme. You'll often want selectedItemColor and unselectedItemColor too, since the defaults can leave unselected icons in a faded grey that looks unintentional. See the icons guide for choosing tab icons.
The Material 3 NavigationBar
On Material 3, NavigationBar is the modern replacement — taller, with a pill indicator behind the selected icon and updated motion. It uses NavigationDestination children and a selectedIndex with onDestinationSelected.
NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
)
The state pattern is identical — track an index, update it on selection — so you can swap one for the other freely. New apps on Material 3 should prefer NavigationBar; BottomNavigationBar remains for Material 2 designs.
Common mistakes
- Rebuilding screens on every switch. Use
IndexedStackto keep each tab's state alive. - Pushing routes for tabs. Tab switching is a state change, not a
Navigator.push. - Forgetting type: fixed with four-plus items. The shifting default hides labels and tints the bar.
- Mismatched item and screen counts. The items list and the IndexedStack children must line up by index.
- Omitting selected/unselected colours. The defaults can render unselected icons in an unintended grey.
Frequently asked questions
How do I add a bottom navigation bar in Flutter?
Put a BottomNavigationBar in the Scaffold's bottomNavigationBar slot, track a selected index in state, and update it in onTap.
How do I keep each tab's state in a Flutter BottomNavigationBar?
Render the screens in an IndexedStack set to the selected index; it keeps every child mounted so state survives switching.
What is the difference between BottomNavigationBar and NavigationBar in Flutter?
BottomNavigationBar is Material 2; NavigationBar is the Material 3 version with a pill indicator. Prefer NavigationBar on new apps.
Why does my Flutter BottomNavigationBar not show labels or colours?
With four or more items it goes shifting and hides labels; set type: BottomNavigationBarType.fixed and the selected/unselected colours.
Further reads
Keep going with the tutorials that pair with this guide:
- Flutter Development Guide 2026 — the full Flutter hub.
- Flutter Navigation Basics — push, pop, and named routes.
- Flutter TabBar and TabBarView — swipeable tabs within a screen.
- Flutter Scaffold Explained — the slot the bottom bar lives in.
- Flutter Icons and IconButton — pick clear, consistent tab icons.
Sources: Flutter documentation — BottomNavigationBar, BottomNavigationBarItem, NavigationBar, NavigationDestination, and IndexedStack API references (docs.flutter.dev). Verified against current stable Flutter.