Flutter: PopupMenuButton Usage and Menu Structures
Last updated:
5 min read

PopupMenuButton is how you offer secondary actions without spending screen space: the user taps the three-dot icon, a small menu opens, one item is chosen and the menu closes. We use it for actions that do not fit in the AppBar, such as "Settings, Help, Sign out", or for the "Edit, Share, Delete" options of a list row. Every item carries a value, and that value is how we find out in onSelected which item was picked.
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
PopupMenuButton<String>(
onSelected: (String value) {
debugPrint('Selected: $value');
},
itemBuilder: (BuildContext context) => const [
PopupMenuItem<String>(
value: 'edit',
child: Text('Edit'),
),
PopupMenuItem<String>(
value: 'delete',
child: Text('Delete'),
),
],
)Without an icon or child, the button is drawn with the platform's "more" icon. Its most common home is the actions list of an AppBar.
Using with Enum
With String values, a typo ('delte' instead of 'delete') slips past the compiler and the menu silently does nothing. An enum as the value type is the healthier choice:
enum MenuAction { edit, delete, share }
PopupMenuButton<MenuAction>(
onSelected: (MenuAction action) {
switch (action) {
case MenuAction.edit:
_editItem();
case MenuAction.delete:
_deleteItem();
case MenuAction.share:
_shareItem();
}
},
itemBuilder: (context) => const [
PopupMenuItem(value: MenuAction.edit, child: Text('Edit')),
PopupMenuItem(value: MenuAction.delete, child: Text('Delete')),
PopupMenuItem(value: MenuAction.share, child: Text('Share')),
],
)In Dart 3 a case with a body no longer needs a break. Even better, when you add a new value to the enum the compiler reminds you about the missing case. What else enums can do is covered in the Dart enum post.
Menu Items with Icons
PopupMenuItem<String>(
value: 'settings',
child: Row(
children: const [
Icon(Icons.settings),
SizedBox(width: 10),
Text('Settings'),
],
),
)PopupMenuDivider and CheckedPopupMenuItem
A divider groups items. Separating the dangerous action (Delete) from the rest is a good habit:
itemBuilder: (context) => const [
PopupMenuItem(value: 'edit', child: Text('Edit')),
PopupMenuItem(value: 'copy', child: Text('Copy')),
PopupMenuDivider(), // Separator line
PopupMenuItem(value: 'delete', child: Text('Delete')),
]When the menu shows a preference rather than an action (a sort order, for instance), use CheckedPopupMenuItem, which marks the current choice with a check:
CheckedPopupMenuItem(
value: SortBy.date,
checked: _sortBy == SortBy.date,
child: const Text('By date'),
)Important Properties
| Property | Description |
|---|---|
onSelected |
Called when an item is selected |
onCanceled |
Called when the menu is dismissed without a selection |
itemBuilder |
Creates menu items |
icon |
Menu button icon |
child |
Custom button widget |
tooltip |
Text shown on long press and read by screen readers |
position |
PopupMenuPosition.over or under |
offset |
Menu position offset |
shape |
Menu shape |
color |
Menu background color |
enabled |
Menu active/inactive state |
Using with Custom Button
PopupMenuButton<String>(
position: PopupMenuPosition.under, // Open the menu below the button
onSelected: (value) {},
itemBuilder: (context) => const [
PopupMenuItem(value: 'week', child: Text('This week')),
PopupMenuItem(value: 'month', child: Text('This month')),
],
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Text('Menu', style: TextStyle(color: Colors.white)),
Icon(Icons.arrow_drop_down, color: Colors.white),
],
),
),
)When to Use It and When Not To
A popup menu hides actions. That is both its strength and its weakness: the screen gets calmer, but the user may never learn the action exists.
- Use it for secondary, rarely used actions, for options that belong to one list row, and for everything beyond the third icon in an AppBar.
- Don't use it for the main action of a screen. Something like "New note" must not be buried in a menu; it deserves a visible button or a FAB. With only one or two actions, plain
IconButtons are better. - Don't use it to pick a value in a form. If the chosen value has to stay visible, the right component is a DropdownButton; a popup menu does not remember a selection, it only triggers an action.
- Don't use it for many options or options that need explanation. On mobile,
showModalBottomSheetgives you more room, closer to the thumb (see the BottomSheet post). If you need submenus on desktop, Material 3'sMenuAnchorwas built for that.
Common Mistakes
1. An item without a value
Symptom: you tap the item, the menu closes, but onSelected is never called.
Cause: an item without a value returns null, and PopupMenuButton reads that as "nothing was selected", so onCanceled runs instead of onSelected.
Fix: give every PopupMenuItem a value and write out the generic type of the button (PopupMenuButton<MenuAction>).
2. Opening a dialog or navigating inside onTap
Symptom: showDialog is called in the onTap parameter of a PopupMenuItem, and on some Flutter versions the dialog flashes and closes immediately.
Cause: onTap runs while the menu route is closing. In older versions, the pop that closed the menu took the freshly opened dialog with it.
Fix: keep onTap for small jobs that need no context, and open dialogs or push pages in onSelected. onSelected is called after the menu route has been popped, so it is safe regardless of the version.
3. Passing both child and icon
Symptom: the assertion "You can only pass [child] or [icon], not both."
Fix: use icon when you only want a different icon, and child when you want to draw the whole button yourself.
Mini Scenario: Edit, Share, Delete on a List Row
Picture a list of notes. Each row ends with a three-dot menu; picking "Delete" first asks for confirmation and then shows a short notification. The row comes first:
enum NoteAction { edit, share, delete }
ListTile(
title: Text(note.title),
trailing: PopupMenuButton<NoteAction>(
tooltip: 'Note options',
onSelected: (action) => _onNoteAction(action, note),
itemBuilder: (context) => const [
PopupMenuItem(value: NoteAction.edit, child: Text('Edit')),
PopupMenuItem(value: NoteAction.share, child: Text('Share')),
PopupMenuDivider(),
PopupMenuItem(value: NoteAction.delete, child: Text('Delete')),
],
),
)Then the method that handles the actions:
Future<void> _onNoteAction(NoteAction action, Note note) async {
switch (action) {
case NoteAction.edit:
await Navigator.push(
context,
MaterialPageRoute(builder: (_) => EditNotePage(note: note)),
);
case NoteAction.share:
// Share logic
break;
case NoteAction.delete:
final ok = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Delete this note?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: const Text('Delete'),
),
],
),
);
if (ok != true || !mounted) return;
setState(() => _notes.remove(note));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Note deleted')),
);
}
}The menu only reports the intent; the work itself lives in one method. That makes it easy to wire the same actions to a swipe gesture or a button on the detail page tomorrow. The details of the confirmation step are in the AlertDialog post. Also note the mounted check after the await: the user may have left the page while the dialog was open.
Frequently Asked Questions
Why is onSelected not called on my PopupMenuButton?
Most likely the selected PopupMenuItem has no value. An item without a value returns null, and in that case onCanceled is called instead of onSelected.
How do I disable a menu item?
Pass enabled: false to the PopupMenuItem. It is drawn faded and cannot be tapped. To disable the whole menu, use the enabled parameter of PopupMenuButton.
How do I open the menu below the button?
By default the menu opens on top of the button. With position: PopupMenuPosition.under it opens right below it, and offset lets you fine-tune the position.
What is the difference between PopupMenuButton and DropdownButton?
PopupMenuButton triggers an action and does not keep the selection. DropdownButton lets the user pick a value and shows the selected value on the button, which is what forms need.
Related Posts
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: BottomSheet and showModalBottomSheet
Flutter's showModalBottomSheet: returning values, isScrollControlled, useSafeArea, showDragHandle, keyboard insets, DraggableScrollableSheet, persistent sheets.
Flutter: SnackBar and SnackBarAction Usage
Flutter SnackBar with ScaffoldMessenger: undo actions, floating style, the queue problem and context errors after an await, all fixed.