← Back to Articles

How to Create a Drawer Navigation in Flutter

How to Create a Drawer Navigation in Flutter

How to Create a Drawer Navigation in Flutter

If you've ever used a mobile app with a hamburger menu icon that slides out a panel from the side, you've experienced a navigation drawer. It's one of the most popular navigation patterns in mobile apps, and Flutter makes it remarkably easy to implement.

In this guide, we'll walk through everything you need to know about creating drawer navigation in Flutter—from a simple basic drawer to a fully customized navigation experience with headers, icons, and smooth transitions.

What is a Drawer?

A Drawer is a panel that slides in horizontally from the edge of a Scaffold to show navigation links and other content. By default, it slides from the left side, though Flutter also supports an EndDrawer that slides from the right.

Drawer Navigation Flow Main Screen AppBar with ☰ Content Tap Drawer • Home • Profile • Settings Dimmed Navigate New

Creating a Basic Drawer

Let's start with the simplest possible drawer. The Scaffold widget has a built-in drawer property that makes this incredibly straightforward.


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Drawer Demo',
      home: const HomeScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My App'),
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: const [
            DrawerHeader(
              child: Text('Menu'),
            ),
            ListTile(
              leading: Icon(Icons.home),
              title: Text('Home'),
            ),
            ListTile(
              leading: Icon(Icons.person),
              title: Text('Profile'),
            ),
            ListTile(
              leading: Icon(Icons.settings),
              title: Text('Settings'),
            ),
          ],
        ),
      ),
      body: const Center(
        child: Text('Welcome to the app!'),
      ),
    );
  }
}

When you add a drawer to a Scaffold, Flutter automatically adds a hamburger menu icon to your AppBar. Tapping it opens the drawer with a smooth slide animation. You can also swipe from the left edge of the screen to open it.

Adding a Custom Drawer Header

The DrawerHeader widget provides space at the top of your drawer for branding or user information. Let's create a more polished header with user details.


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

  @override
  Widget build(BuildContext context) {
    debugPrint('CustomDrawerHeader.build: Building drawer header');
    
    return UserAccountsDrawerHeader(
      accountName: const Text('Jane Developer'),
      accountEmail: const Text('jane@example.com'),
      currentAccountPicture: CircleAvatar(
        backgroundColor: Colors.white,
        child: Text(
          'J',
          style: TextStyle(
            fontSize: 40.0,
            color: Theme.of(context).primaryColor,
          ),
        ),
      ),
      otherAccountsPictures: const [
        CircleAvatar(
          backgroundColor: Colors.white,
          child: Text('A'),
        ),
      ],
      decoration: BoxDecoration(
        color: Theme.of(context).primaryColor,
      ),
    );
  }
}

The UserAccountsDrawerHeader is a specialized widget designed for displaying user information. It handles the layout of profile pictures and account details beautifully out of the box.

Creating Navigation Items

Now let's build a reusable drawer item widget that handles navigation properly. When a user taps a menu item, we want to close the drawer and navigate to the selected screen.


class DrawerItem extends StatelessWidget {
  final IconData icon;
  final String title;
  final VoidCallback onTap;

  const DrawerItem({
    super.key,
    required this.icon,
    required this.title,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    debugPrint('DrawerItem.build: Building drawer item - $title');
    
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      onTap: () {
        debugPrint('DrawerItem.build: Tapped on $title');
        Navigator.pop(context); // Close the drawer first
        onTap(); // Then execute the navigation
      },
    );
  }
}

Notice how we call Navigator.pop(context) before executing the onTap callback. This closes the drawer before navigating, creating a smoother user experience.

Building a Complete Navigation Drawer

Let's put everything together into a complete, reusable navigation drawer component.

Drawer Widget Structure Drawer ListView DrawerHeader User info / Branding ListTile(s) Navigation items Divider Section separator

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    debugPrint('AppDrawer.build: Building main app drawer');
    
    return Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          const UserAccountsDrawerHeader(
            accountName: Text('Jane Developer'),
            accountEmail: Text('jane@example.com'),
            currentAccountPicture: CircleAvatar(
              backgroundColor: Colors.white,
              child: Icon(Icons.person, size: 50),
            ),
          ),
          ListTile(
            leading: const Icon(Icons.home),
            title: const Text('Home'),
            onTap: () {
              debugPrint('AppDrawer.build: Navigating to Home');
              Navigator.pop(context);
              Navigator.pushReplacementNamed(context, '/');
            },
          ),
          ListTile(
            leading: const Icon(Icons.person),
            title: const Text('Profile'),
            onTap: () {
              debugPrint('AppDrawer.build: Navigating to Profile');
              Navigator.pop(context);
              Navigator.pushNamed(context, '/profile');
            },
          ),
          ListTile(
            leading: const Icon(Icons.favorite),
            title: const Text('Favorites'),
            onTap: () {
              debugPrint('AppDrawer.build: Navigating to Favorites');
              Navigator.pop(context);
              Navigator.pushNamed(context, '/favorites');
            },
          ),
          const Divider(),
          ListTile(
            leading: const Icon(Icons.settings),
            title: const Text('Settings'),
            onTap: () {
              debugPrint('AppDrawer.build: Navigating to Settings');
              Navigator.pop(context);
              Navigator.pushNamed(context, '/settings');
            },
          ),
          ListTile(
            leading: const Icon(Icons.help),
            title: const Text('Help & Feedback'),
            onTap: () {
              debugPrint('AppDrawer.build: Navigating to Help');
              Navigator.pop(context);
              Navigator.pushNamed(context, '/help');
            },
          ),
        ],
      ),
    );
  }
}

Highlighting the Selected Item

A polished drawer should show which screen is currently active. Let's enhance our drawer to highlight the selected navigation item.


class NavigationDrawer extends StatelessWidget {
  final String currentRoute;

  const NavigationDrawer({
    super.key,
    required this.currentRoute,
  });

  @override
  Widget build(BuildContext context) {
    debugPrint('NavigationDrawer.build: Building with currentRoute: $currentRoute');
    
    return Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          const DrawerHeader(
            decoration: BoxDecoration(
              color: Colors.blue,
            ),
            child: Text(
              'My Application',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
              ),
            ),
          ),
          _buildNavItem(
            context: context,
            icon: Icons.home,
            title: 'Home',
            route: '/',
          ),
          _buildNavItem(
            context: context,
            icon: Icons.dashboard,
            title: 'Dashboard',
            route: '/dashboard',
          ),
          _buildNavItem(
            context: context,
            icon: Icons.notifications,
            title: 'Notifications',
            route: '/notifications',
          ),
        ],
      ),
    );
  }

  Widget _buildNavItem({
    required BuildContext context,
    required IconData icon,
    required String title,
    required String route,
  }) {
    final bool isSelected = currentRoute == route;
    debugPrint('NavigationDrawer._buildNavItem: $title isSelected: $isSelected');

    return ListTile(
      leading: Icon(
        icon,
        color: isSelected ? Theme.of(context).primaryColor : null,
      ),
      title: Text(
        title,
        style: TextStyle(
          color: isSelected ? Theme.of(context).primaryColor : null,
          fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
        ),
      ),
      selected: isSelected,
      onTap: () {
        debugPrint('NavigationDrawer._buildNavItem: Tapped $title');
        Navigator.pop(context);
        if (!isSelected) {
          Navigator.pushReplacementNamed(context, route);
        }
      },
    );
  }
}

Using EndDrawer

Sometimes you need a drawer that slides in from the right side instead of the left. Flutter provides the endDrawer property for this purpose.


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

  @override
  Widget build(BuildContext context) {
    debugPrint('ScreenWithEndDrawer.build: Building screen with end drawer');
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('End Drawer Example'),
        actions: [
          Builder(
            builder: (context) => IconButton(
              icon: const Icon(Icons.filter_list),
              onPressed: () {
                debugPrint('ScreenWithEndDrawer.build: Opening end drawer');
                Scaffold.of(context).openEndDrawer();
              },
            ),
          ),
        ],
      ),
      endDrawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              child: Text('Filters'),
            ),
            CheckboxListTile(
              title: const Text('Show Active Only'),
              value: true,
              onChanged: (value) {
                debugPrint('ScreenWithEndDrawer.build: Filter changed');
              },
            ),
            CheckboxListTile(
              title: const Text('Sort by Date'),
              value: false,
              onChanged: (value) {
                debugPrint('ScreenWithEndDrawer.build: Sort changed');
              },
            ),
          ],
        ),
      ),
      body: const Center(
        child: Text('Swipe from right or tap filter icon'),
      ),
    );
  }
}

End drawers are perfect for filters, settings panels, or secondary actions that don't need to be in the main navigation.

Programmatically Opening and Closing Drawers

You can control drawers programmatically using ScaffoldState. This is useful when you want to open a drawer from a button or other widget.


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

  @override
  Widget build(BuildContext context) {
    debugPrint('DrawerControlExample.build: Building drawer control example');
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('Drawer Control'),
        leading: Builder(
          builder: (context) => IconButton(
            icon: const Icon(Icons.menu),
            onPressed: () {
              debugPrint('DrawerControlExample.build: Opening drawer via button');
              Scaffold.of(context).openDrawer();
            },
          ),
        ),
      ),
      drawer: const Drawer(
        child: Center(
          child: Text('Drawer Content'),
        ),
      ),
      body: Builder(
        builder: (context) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  debugPrint('DrawerControlExample.build: Opening drawer from body');
                  Scaffold.of(context).openDrawer();
                },
                child: const Text('Open Drawer'),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  debugPrint('DrawerControlExample.build: Checking if drawer is open');
                  final isOpen = Scaffold.of(context).isDrawerOpen;
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Drawer is ${isOpen ? "open" : "closed"}')),
                  );
                },
                child: const Text('Check Drawer State'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Creating a Custom Drawer Width

By default, drawers take up about 304 logical pixels or 85% of the screen width (whichever is smaller). You can customize this by wrapping your Drawer content in a SizedBox or Container.


Drawer(
  width: 250, // Custom width
  child: ListView(
    children: [
      // Your drawer content
    ],
  ),
)

Adding a Drawer to Multiple Screens

For apps with multiple screens that all need the same drawer, create a reusable shell widget.


class AppShell extends StatelessWidget {
  final String title;
  final Widget body;
  final String currentRoute;

  const AppShell({
    super.key,
    required this.title,
    required this.body,
    required this.currentRoute,
  });

  @override
  Widget build(BuildContext context) {
    debugPrint('AppShell.build: Building shell for route: $currentRoute');
    
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      drawer: NavigationDrawer(currentRoute: currentRoute),
      body: body,
    );
  }
}

// Usage in different screens:
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    debugPrint('HomeScreen.build: Building home screen');
    
    return AppShell(
      title: 'Home',
      currentRoute: '/',
      body: const Center(
        child: Text('Home Content'),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    debugPrint('DashboardScreen.build: Building dashboard screen');
    
    return AppShell(
      title: 'Dashboard',
      currentRoute: '/dashboard',
      body: const Center(
        child: Text('Dashboard Content'),
      ),
    );
  }
}
App Shell Pattern AppShell Drawer Body uses HomeScreen Dashboard Settings Same drawer across all screens

Best Practices

Here are some tips to make your drawer navigation even better:

  • Keep it simple: Don't overload your drawer with too many options. Aim for 5-7 main navigation items.
  • Use icons consistently: Every menu item should have an icon for quick visual recognition.
  • Group related items: Use Divider widgets to separate different sections of your menu.
  • Show current location: Always highlight which screen the user is currently on.
  • Close before navigating: Always close the drawer before pushing a new route to avoid visual glitches.
  • Consider accessibility: Ensure your drawer items have proper semantic labels for screen readers.

Summary

Creating drawer navigation in Flutter is straightforward thanks to the built-in Drawer widget. We've covered:

  • Creating basic drawers with the Scaffold's drawer property
  • Adding custom headers with DrawerHeader and UserAccountsDrawerHeader
  • Building navigation items that properly close the drawer before navigating
  • Highlighting the current route for better user feedback
  • Using endDrawer for right-side panels
  • Programmatically controlling drawers with Scaffold.of(context)
  • Creating a reusable app shell for consistent navigation across screens

With these techniques, you can build professional, intuitive navigation experiences that feel native to mobile users. The drawer pattern works wonderfully for apps with multiple top-level destinations, and Flutter's implementation makes it easy to customize every aspect of the experience.

Learning Points

Here are some advanced programming concepts to take away from this article:

  • Builder Pattern: We used the Builder widget to obtain the correct BuildContext when accessing Scaffold.of(context). This is essential when the Scaffold and the widget trying to access it are in the same build method.
  • Composition over Inheritance: The AppShell pattern demonstrates composition—wrapping common functionality in a reusable widget rather than creating complex inheritance hierarchies.
  • State Location: Notice how the currentRoute parameter flows down to highlight the correct item. This is a form of lifting state up, making the drawer stateless and more predictable.
  • Navigation Stack Management: Using pushReplacementNamed instead of pushNamed prevents the navigation stack from growing infinitely when switching between main screens.
  • Debug Logging: Strategic debugPrint statements following the ClassName.methodName pattern help trace execution flow during development without cluttering release builds.