Navigation & Routing

Drawer Navigation

15 min Lesson 6 of 14

Drawer Navigation in Flutter

A Drawer is a side panel that slides in from the left (or right) edge of the screen, providing access to primary navigation destinations in your app. It is a Material Design pattern that works especially well on mobile devices where screen space is limited. Flutter makes it simple to add a fully functional drawer to any Scaffold by setting its drawer property.

Drawers are ideal when your app has five or more top-level destinations that would otherwise clutter a bottom navigation bar, or when you want to keep the main content area clean and distraction-free. The user opens the drawer by swiping from the left edge of the screen or tapping the hamburger icon that Flutter automatically places in the AppBar.

Note: Flutter automatically adds a hamburger menu icon to the AppBar when a drawer is provided to the Scaffold. You do not need to add the icon manually. Use endDrawer instead of drawer if you want the panel to slide in from the right.

Basic Drawer Structure

A drawer is built using the Drawer widget, which typically wraps a ListView containing a DrawerHeader (or UserAccountsDrawerHeader) followed by a series of ListTile widgets — one for each navigation destination.

Minimal Drawer Example

import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => const HomeScreen(),
        '/profile': (context) => const ProfileScreen(),
        '/settings': (context) => const SettingsScreen(),
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      drawer: Drawer(
        child: ListView(
          // Remove the default top padding so DrawerHeader sits flush
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(color: Colors.indigo),
              child: Text(
                'My App',
                style: TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.home),
              title: const Text('Home'),
              onTap: () {
                Navigator.pop(context); // Close the drawer first
                Navigator.pushReplacementNamed(context, '/');
              },
            ),
            ListTile(
              leading: const Icon(Icons.person),
              title: const Text('Profile'),
              onTap: () {
                Navigator.pop(context);
                Navigator.pushReplacementNamed(context, '/profile');
              },
            ),
            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('Settings'),
              onTap: () {
                Navigator.pop(context);
                Navigator.pushReplacementNamed(context, '/settings');
              },
            ),
          ],
        ),
      ),
      body: const Center(child: Text('Home Screen')),
    );
  }
}
Tip: Always call Navigator.pop(context) before navigating to close the drawer smoothly. If you navigate without closing, the drawer will remain on top of the new screen until the user manually dismisses it.

Highlighting the Active Destination

Good UX requires the drawer to visually indicate which screen the user is currently on. You can achieve this by tracking a selected index (or selected route name) in a StatefulWidget and using the selected property of ListTile along with selectedTileColor to highlight the active item.

Drawer with Active Item Highlighting

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

  @override
  State<MainScaffold> createState() => _MainScaffoldState();
}

class _MainScaffoldState extends State<MainScaffold> {
  // Track which destination is currently active
  int _selectedIndex = 0;

  // Parallel lists: screens and their drawer metadata
  final List<Widget> _screens = const [
    HomeScreen(),
    ProfileScreen(),
    SettingsScreen(),
  ];

  final List<Map<String, dynamic>> _navItems = const [
    {'icon': Icons.home, 'label': 'Home'},
    {'icon': Icons.person, 'label': 'Profile'},
    {'icon': Icons.settings, 'label': 'Settings'},
  ];

  void _navigateTo(int index) {
    setState(() {
      _selectedIndex = index;
    });
    Navigator.pop(context); // Close drawer
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_navItems[_selectedIndex]['label'] as String),
        backgroundColor: Colors.indigo,
        foregroundColor: Colors.white,
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            UserAccountsDrawerHeader(
              accountName: const Text('Edrees Salih'),
              accountEmail: const Text('edrees@example.com'),
              currentAccountPicture: const CircleAvatar(
                backgroundColor: Colors.white,
                child: Icon(Icons.person, color: Colors.indigo, size: 40),
              ),
              decoration: const BoxDecoration(color: Colors.indigo),
            ),
            ...List.generate(_navItems.length, (index) {
              final item = _navItems[index];
              return ListTile(
                leading: Icon(item['icon'] as IconData),
                title: Text(item['label'] as String),
                selected: _selectedIndex == index,
                selectedTileColor: Colors.indigo.withOpacity(0.12),
                selectedColor: Colors.indigo,
                onTap: () => _navigateTo(index),
              );
            }),
            const Divider(),
            ListTile(
              leading: const Icon(Icons.logout),
              title: const Text('Sign Out'),
              onTap: () {
                Navigator.pop(context);
                // Handle sign-out logic
              },
            ),
          ],
        ),
      ),
      body: _screens[_selectedIndex],
    );
  }
}

UserAccountsDrawerHeader

The UserAccountsDrawerHeader widget is a Material-standard drawer header designed to display a user's avatar, name, and email address. It automatically handles tap-to-switch-account interaction and adapts to the app's theme. Key properties include:

  • accountName — the primary user name widget
  • accountEmail — the email or secondary text widget
  • currentAccountPicture — usually a CircleAvatar
  • decoration — background, gradient, or image for the header
  • otherAccountsPictures — small avatars for additional accounts

Opening and Closing the Drawer Programmatically

Sometimes you need to open or close the drawer from code rather than through user interaction. Use a GlobalKey<ScaffoldState> to get a reference to the Scaffold and call openDrawer() or closeDrawer().

Programmatic Drawer Control

class MyPage extends StatelessWidget {
  // Assign a key to the Scaffold
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  MyPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: const Text('Dashboard'),
        // Custom button to open the drawer
        leading: IconButton(
          icon: const Icon(Icons.menu),
          onPressed: () => _scaffoldKey.currentState?.openDrawer(),
        ),
      ),
      drawer: const Drawer(
        child: Center(child: Text('Navigation Here')),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => _scaffoldKey.currentState?.openDrawer(),
          child: const Text('Open Menu'),
        ),
      ),
    );
  }
}
Warning: Do not provide both a custom leading widget and a drawer to the Scaffold without also assigning an automaticallyImplyLeading: false override if you want full control. By default, Flutter will show the hamburger icon automatically and your custom leading widget will replace it.

Drawer Width and Appearance

The default drawer width is 304 logical pixels (per Material guidelines). You can customise the appearance through the Drawer properties:

  • width — set a custom width (e.g. 280)
  • backgroundColor — background color of the drawer panel
  • shape — rounded corners or other border shapes
  • elevation — shadow depth (default 16)

Summary

The Scaffold drawer property accepts any widget, but the conventional pattern is DrawerListView (with padding: EdgeInsets.zero) → header widget + ListTile items. Track the selected destination index in a StatefulWidget, pass it to each ListTile's selected property, and call Navigator.pop(context) before navigating to give a smooth user experience. For programmatic control, use a GlobalKey<ScaffoldState>.