İçeriğe geç / Skip to content / Zum Inhalt

Flutter: PopupMenuButton Usage and Menu Structures

Ahmet Balaman

Last updated:

5 min read

FlutterPopupMenuButtonPopupMenuItemMenuAppBarUI
Flutter: PopupMenuButton Usage and Menu Structures

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.

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, showModalBottomSheet gives you more room, closer to the thumb (see the BottomSheet post). If you need submenus on desktop, Material 3's MenuAnchor was 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.

Comments