Flutter: Drawer Widget and Side Menu Usage
Last updated:
5 min read

Drawer is the side menu that slides in from the edge of the screen. It suits apps with more sections than a bottom bar can hold, or apps that want secondary destinations such as settings, help and account in one place. The moment you give a Scaffold a drawer, the AppBar gets a menu icon, the edge swipe works and the back button closes the menu. What is left for you is filling the content and deciding what happens after a selection.
Live Demo
You can try this widget in the interactive example below:
💡 If the example above doesn't load, click DartPad to run it in a new tab.
Basic Usage
Scaffold(
appBar: AppBar(title: const Text('Drawer Example')),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('Menu Header'),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('Home'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: () {
Navigator.pop(context);
},
),
],
),
),
body: const Center(child: Text('Content')),
)Make a ListView your default content: when the menu grows or the phone is held in landscape, you won't hit an overflow error. Navigator.pop(context) here closes the open drawer, not the page.
DrawerHeader and UserAccountsDrawerHeader
DrawerHeader is the free-form area at the top of the menu; any widget can go inside. If you want to show a user name, email and avatar, the ready-made UserAccountsDrawerHeader saves work:
UserAccountsDrawerHeader(
accountName: const Text('User Name'),
accountEmail: const Text('[email protected]'),
currentAccountPicture: const CircleAvatar(child: Text('UN')),
decoration: const BoxDecoration(color: Colors.blue),
)Opening and Closing Programmatically
There are two ways to open the menu from your own button. The first is giving the Scaffold a GlobalKey:
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: _scaffoldKey,
drawer: const Drawer(),
body: ElevatedButton(
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
child: const Text('Open Drawer'),
),
)The second is Scaffold.of(context).openDrawer(), which comes with a context trap that I explain in the mistakes section below. To close, use Navigator.pop(context) or _scaffoldKey.currentState?.closeDrawer().
EndDrawer (Right Side)
endDrawer opens the same widget from the right edge. While the left menu is for navigation, the right side usually carries screen-specific content such as a filter panel. Call openEndDrawer() to open it.
Important Properties
| Property | Description |
|---|---|
drawer |
Left side drawer |
endDrawer |
Right side drawer |
drawerScrimColor |
Color that dims the background while the menu is open |
drawerEdgeDragWidth |
Width of the area where the edge swipe is detected |
drawerEnableOpenDragGesture |
Turns opening by swipe on or off |
Drawer(width: ...) |
Width of the menu |
Note that these are parameters of Scaffold, not of Drawer, with the exception of the width.
Material 3: NavigationDrawer
NavigationDrawer, which arrived with Material 3, highlights the selected item with a pill shape and drives selection through selectedIndex and onDestinationSelected. Instead of styling ListTiles by hand, you list destinations:
NavigationDrawer(
selectedIndex: _index,
onDestinationSelected: (index) {
Navigator.pop(context);
setState(() => _index = index);
},
children: const [
Padding(
padding: EdgeInsets.fromLTRB(28, 16, 16, 10),
child: Text('Menu'),
),
NavigationDrawerDestination(
icon: Icon(Icons.inbox_outlined),
selectedIcon: Icon(Icons.inbox),
label: Text('Inbox'),
),
NavigationDrawerDestination(
icon: Icon(Icons.send_outlined),
selectedIcon: Icon(Icons.send),
label: Text('Sent'),
),
],
)children may also contain non-destination widgets such as a headline or a Divider; the index counts only the NavigationDrawerDestinations. The menu does not close itself after a selection, so Navigator.pop is still your job.
When to Use It and When Not To
A drawer fits apps with more than five top-level sections, or apps that want rarely used destinations (settings, about, sign out) out of sight. The price is that the menu is hidden: the user has to tap the icon before seeing the sections at all.
- With three to five sections of equal weight, a BottomNavigationBar is better; destinations are always visible and one tap away.
- For sibling content of the same screen (Active / Completed), use a TabBar, not a drawer.
- At tablet and desktop widths a
NavigationRailor a permanent side panel beats a hidden menu. - If you combine a bottom bar with a drawer, don't put the same destination in both; primary sections go to the bottom, secondary ones into the drawer.
Common Mistakes
1. Scaffold.of() called with a context that does not contain a Scaffold. This error appears when you call Scaffold.of with the context of the build method that creates the Scaffold. That context sits above the Scaffold in the tree and cannot search downwards. Insert a Builder to get a new context from below the Scaffold:
Scaffold(
drawer: const Drawer(),
body: Builder(
builder: (innerContext) => ElevatedButton(
onPressed: () => Scaffold.of(innerContext).openDrawer(),
child: const Text('Open Menu'),
),
),
)Moving the button into its own widget class has the same effect, because that class's build method receives its own context.
2. Navigating without closing the menu. Symptom: when the user comes back from the opened page, the drawer is still open. In onTap, close the menu first, then navigate:
onTap: () {
Navigator.pop(context); // closes the drawer
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SettingsPage()),
);
}3. An empty strip above the header. ListView adds top padding equal to the status bar by default, so the color of the DrawerHeader does not reach the very top of the screen. padding: EdgeInsets.zero fixes that.
4. Horizontally scrolling content opens the menu. If the page has a map, a horizontal list or a swipeable gallery, a gesture near the left edge can trigger the drawer. Set drawerEnableOpenDragGesture: false on the Scaffold so the menu opens only through the icon.
Mini Scenario: A Menu That Switches Sections and Opens Settings
Picture a student panel: Lessons, Homework and Grades swap the body inside the same Scaffold, while Settings opens as a separate page. The two behaviors differ, and the menu has to get both right:
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _index = 0;
static const _titles = ['Lessons', 'Homework', 'Grades'];
static const _icons = [Icons.school, Icons.assignment, Icons.grade];
void _selectSection(int index) {
Navigator.pop(context); // the drawer closes first
setState(() => _index = index);
}
void _openSettings() {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SettingsPage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(_titles[_index])),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.indigo),
child: Text(
'Student Panel',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
for (var i = 0; i < _titles.length; i++)
ListTile(
leading: Icon(_icons[i]),
title: Text(_titles[i]),
selected: i == _index,
onTap: () => _selectSection(i),
),
const Divider(),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: _openSettings,
),
],
),
),
body: Center(child: Text('${_titles[_index]} content')),
);
}
}
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: const Center(child: Text('Settings')),
);
}
}Selecting a section opens no new page; only _index changes, so the back button leaves the app instead of stepping through sections. selected: i == _index shows users where they are. Settings, on the other hand, is a real page transition: its AppBar gets a back arrow, and on return the menu is closed because pop was called first.
Frequently Asked Questions
How do I open a drawer on the right side?
Use the endDrawer parameter of Scaffold. If actions is empty, the AppBar adds the menu icon on the right by itself; to open it from your own button, call openEndDrawer().
How do I change the drawer icon?
Put your own IconButton into the leading slot of the AppBar and call Scaffold.of(context).openDrawer() in onPressed. Wrap the button in a Builder so the context comes from below the Scaffold.
What is the difference between NavigationDrawer and Drawer?
Drawer is an empty panel; you build the content and the selected look yourself. NavigationDrawer ships the Material 3 look and the selected-item highlight, and manages selection through an index.
What does the back button do while the drawer is open?
It closes the menu first and the page stays where it is. When the menu is closed, the normal back behavior applies.
Related Posts
Flutter: TabBar and TabBarView Usage
Flutter TabBar and TabBarView: DefaultTabController vs your own TabController, keeping tab state alive, Material 3 settings, and common mistakes.
Flutter: AppBar Widget and Customization
A practical Flutter AppBar guide: title, leading, actions and bottom, the Material 3 scrolled-under color change, and fixes for common mistakes.
Flutter: BottomNavigationBar vs NavigationBar
Migrating from BottomNavigationBar to Material 3 NavigationBar: property mapping, labelBehavior, a decision guide and an adaptive shell example.