Reader Stacks

How to Build Drawer Navigation in Flutter

Use NavigationDrawer for Material 3 destinations, close the drawer before routing, preserve accessible selection, and move to persistent navigation on wider layouts.

How to Build Drawer Navigation in Flutter

For primary navigation in a Material 3 Flutter app, prefer NavigationDrawer; Flutter's Drawer API explicitly points Material 3 applications toward the newer component. Both can be hosted in Scaffold.drawer. Track the selected destination in state, close the drawer before pushing or replacing a route, keep navigation safe from system insets and large text, and switch to a persistent pattern such as NavigationRail on wide layouts instead of hiding primary navigation behind a hamburger button everywhere.

Material 3 NavigationDrawer

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

  @override
  State<AppShell> createState() => _AppShellState();
}

class _AppShellState extends State<AppShell> {
  int selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ReaderStacks')),
      drawer: NavigationDrawer(
        selectedIndex: selectedIndex,
        onDestinationSelected: (index) {
          setState(() {
            selectedIndex = index;
          });

          Navigator.pop(context);
        },
        children: const [
          NavigationDrawerDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: Text('Home'),
          ),
          NavigationDrawerDestination(
            icon: Icon(Icons.settings_outlined),
            selectedIcon: Icon(Icons.settings),
            label: Text('Settings'),
          ),
        ],
      ),
      body: IndexedStack(
        index: selectedIndex,
        children: const [
          Center(child: Text('Home')),
          Center(child: Text('Settings')),
        ],
      ),
    );
  }
}

selectedIndex represents the selected destination. The callback updates state and then closes the drawer.

Route navigation: close first

onTap: () {
  Navigator.pop(context);
  Navigator.pushNamed(context, '/settings');
}

Closing the drawer first prevents it from remaining over the newly navigated page or creating confusing back-stack behavior. In a larger app, centralize routing rather than scattering route strings through menu tiles.

Classic Drawer remains supported

Scaffold(
  appBar: AppBar(title: const Text('My App')),
  drawer: Drawer(
    child: SafeArea(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          const DrawerHeader(child: Text('Menu')),
          ListTile(
            leading: const Icon(Icons.home),
            title: const Text('Home'),
            onTap: () {
              Navigator.pop(context);
            },
          ),
        ],
      ),
    ),
  ),
  body: const Center(child: Text('Content')),
)

Drawer is not removed. It remains appropriate for custom side-panel content and existing Material 2-style designs. The distinction is current Material guidance, not widget availability.

AppBar and the hamburger button

When a Scaffold has a drawer and the AppBar has no explicit leading widget, Flutter normally supplies the menu control. If you override leading or build a custom top bar, preserve a visible, focusable, semantically understandable way to open navigation. Edge swipe alone is not enough for discoverability.

Open the drawer programmatically without defaulting to a GlobalKey

Builder(
  builder: (context) {
    return IconButton(
      tooltip: 'Open navigation',
      icon: const Icon(Icons.menu),
      onPressed: () {
        Scaffold.of(context).openDrawer();
      },
    );
  },
)

A GlobalKey<ScaffoldState> is also possible, but ordinary Scaffold context is simpler when available.

SafeArea, scrolling, and large text

Custom navigation content should remain clear of system insets and reachable when labels wrap or text scaling increases. A classic Drawer commonly uses a scrollable ListView; use SafeArea where custom content would collide with system UI.

NavigationDrawer child/index caution

A NavigationDrawer can contain more than destination widgets, such as headers/dividers. If you mix them, keep selectedIndex aligned with actual destinations rather than assuming every child is selectable.

Wide layouts: consider NavigationRail

Widget buildNavigation(BuildContext context) {
  final width = MediaQuery.sizeOf(context).width;

  if (width >= 900) {
    return NavigationRail(
      selectedIndex: selectedIndex,
      onDestinationSelected: (index) {
        setState(() => selectedIndex = index);
      },
      destinations: const [
        NavigationRailDestination(
          icon: Icon(Icons.home_outlined),
          selectedIcon: Icon(Icons.home),
          label: Text('Home'),
        ),
        NavigationRailDestination(
          icon: Icon(Icons.settings_outlined),
          selectedIcon: Icon(Icons.settings),
          label: Text('Settings'),
        ),
      ],
    );
  }

  return const SizedBox.shrink();
}

900 pixels is an editorial example, not a Flutter-prescribed universal breakpoint. Choose breakpoints from actual layout/content testing.

Accessibility checks

  • Give every destination a clear text label.
  • Use the component's selected state rather than only a custom color.
  • Ensure the open-navigation control has a usable tooltip/semantics.
  • Verify keyboard and focus traversal on desktop/web.
  • Test large text and translated labels.
  • Check screen-reader announcement of selected destinations.

Choose the right navigation component

  • New Material 3 drawer navigation: NavigationDrawer.
  • Custom/arbitrary side panel or existing M2 UI: Drawer.
  • Persistent primary nav on wider layouts: NavigationRail.
  • Compact bottom navigation: consider NavigationBar instead of forcing a drawer everywhere.

Common mistakes

  • Navigating before closing the drawer.
  • Overriding AppBar.leading and removing the only visible open control.
  • Icon-only destinations with unclear meaning.
  • Keeping desktop primary nav hidden behind a hamburger unnecessarily.
  • Using a GlobalKey when normal Scaffold context is enough.
  • Copying a breakpoint from a tutorial without testing the real layout.

Related guides

Sources and further reading