Dart Tutorial

Flutter Lesson 23 of 83 3 min read

NavigationBar, Drawer and Tabs in Flutter

Learn the three ways to switch between sections in Flutter: bottom NavigationBar, side Drawer, and TabBar with TabBarView.

On this page

An app with several top-level sections needs a way to move between them. Flutter offers three patterns.

PatternBest for
Bottom NavigationBarThree to five main sections on a phone
DrawerMany destinations, or secondary ones such as Settings and Help
TabBarRelated views of the same content, such as Chats, Calls and Status

Bottom navigation #

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: HomeShell()));

class HomeShell extends StatefulWidget {
  const HomeShell({super.key});

  @override
  State<HomeShell> createState() => _HomeShellState();
}

class _HomeShellState extends State<HomeShell> {
  int _index = 0;

  static const _pages = [
    Center(child: Text('Home')),
    Center(child: Text('Search')),
    Center(child: Text('Profile')),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(index: _index, children: _pages),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _index,
        onDestinationSelected: (i) => setState(() => _index = i),
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: 'Home',
          ),
          NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
          NavigationDestination(
            icon: Icon(Icons.person_outline),
            selectedIcon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }
}

The bar only reports which item was tapped. You store the index and show the matching page.

IndexedStack keeps every page alive, so each tab remembers its scroll position. If you write body: _pages[_index] instead, a page is rebuilt from scratch each time you return to it.

NavigationBar is the Material 3 widget. The older BottomNavigationBar still works and you will see it in existing code.

Drawer #

Scaffold(
  appBar: AppBar(title: const Text('My app')),
  drawer: NavigationDrawer(
    selectedIndex: _index,
    onDestinationSelected: (i) {
      setState(() => _index = i);
      Navigator.pop(context); // close the drawer
    },
    children: const [
      Padding(
        padding: EdgeInsets.fromLTRB(28, 16, 16, 10),
        child: Text('Mail'),
      ),
      NavigationDrawerDestination(icon: Icon(Icons.inbox), label: Text('Inbox')),
      NavigationDrawerDestination(icon: Icon(Icons.send), label: Text('Sent')),
      Divider(indent: 28, endIndent: 28),
      NavigationDrawerDestination(icon: Icon(Icons.settings), label: Text('Settings')),
    ],
  ),
  body: const Center(child: Text('Content')),
)

Adding a drawer puts a menu button in the app bar automatically. The user can also swipe from the edge.

For a fully custom drawer, use Drawer(child: ListView(children: [...])) with a DrawerHeader and ListTiles.

Tabs #

DefaultTabController links a TabBar and a TabBarView so they stay in step, including swipes.

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: DefaultTabController(
        length: 3,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('Messages'),
            bottom: const TabBar(
              tabs: [
                Tab(text: 'Chats'),
                Tab(text: 'Calls'),
                Tab(icon: Icon(Icons.group), text: 'Groups'),
              ],
            ),
          ),
          body: const TabBarView(
            children: [
              Center(child: Text('Chats')),
              Center(child: Text('Calls')),
              Center(child: Text('Groups')),
            ],
          ),
        ),
      ),
    ),
  );
}

The length, the number of tabs and the number of views must all match. For many tabs, set isScrollable: true on the TabBar.

A bottom bar stretched across a tablet or desktop looks wrong. On wide screens, show a NavigationRail down the side.

Row(
  children: [
    NavigationRail(
      selectedIndex: _index,
      onDestinationSelected: (i) => setState(() => _index = i),
      labelType: NavigationRailLabelType.all,
      destinations: const [
        NavigationRailDestination(icon: Icon(Icons.home), label: Text('Home')),
        NavigationRailDestination(icon: Icon(Icons.search), label: Text('Search')),
        NavigationRailDestination(icon: Icon(Icons.person), label: Text('Profile')),
      ],
    ),
    const VerticalDivider(width: 1),
    Expanded(child: _pages[_index]),
  ],
)

Switching between the bar and the rail by screen width is shown in responsive layouts.

Guidelines #

  • Use three to five destinations in a bottom bar. Fewer than three does not need one; more than five does not fit.
  • Always show labels. Icons alone are ambiguous.
  • Do not combine a bottom bar and tabs for the same level of navigation.
  • Keep the same destinations in the same order on every screen.

Try it yourself #

Build an app shell with four bottom destinations (Home, Orders, Wishlist, Account). Make the Orders page contain two tabs, “Active” and “Past”.

Practise in the playground Updated by Santosh Adhikari