Skip to content
Blog

PopupMenuButton Patterns in Flutter: Overflow Menus Done Right

PopupMenuButton vs PopupMenu, itemBuilder contracts, onSelected and onCanceled, initialValue checks, and the menu-button pattern desktop apps expect.

Published on • November 4, 2026

AI Assistant

Every app has that three-dot menu: sort, refresh, settings. Flutter’s PopupMenuButton builds it for you — an anchor widget that opens a PopupMenu positioned against itself (docs.flutter.dev).

The Standard Overflow Menu

AppBar(
  actions: [
    PopupMenuButton<String>(
      onSelected: (value) {
        switch (value) {
          case 'refresh': _refresh();
          case 'settings': context.go('/settings');
        }
      },
      itemBuilder: (context) => const [
        PopupMenuItem(value: 'refresh', child: Text('Refresh')),
        PopupMenuItem(value: 'settings', child: Text('Settings')),
      ],
    ),
  ],
)

itemBuilder runs every time the menu opens — build items from current state, not from a cached list. Each PopupMenuItem needs a value; that value is what onSelected receives.

Checked Items With initialValue

For menus that reflect current selection (view mode, sort order), pass initialValue and mark the active entry:

PopupMenuButton<String>(
  initialValue: _sortOrder,
  onSelected: (v) => setState(() => _sortOrder = v),
  itemBuilder: (context) => const [
    PopupMenuItem(
      value: 'name',
      child: ListTile(
        leading: Icon(Icons.sort_by_alpha),
        title: Text('Name'),
        contentPadding: EdgeInsets.zero,
      ),
    ),
    PopupMenuItem(
      value: 'date',
      child: ListTile(
        leading: Icon(Icons.schedule),
        title: Text('Date'),
        contentPadding: EdgeInsets.zero,
      ),
    ),
  ],
)

The selected item gets a checkmark via CheckedPopupMenuItem semantics; using a ListTile inside gives you icon + label rows that match Material’s menu spec (48dp minimum height comes free).

Icon-Only Anchors

The button itself is customizable through icon, child, or by wrapping your own widget with MenuItemButton-style anchors:

PopupMenuButton<String>(
  tooltip: 'More actions',          // accessibility, not optional
  icon: const Icon(Icons.more_vert),
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  elevation: 2,
  onSelected: _handle,
  itemBuilder: _buildItems,
)

Always set tooltip on icon-only anchors — it becomes the semantic label for screen readers and the hover hint on desktop.

onCanceled and the Dismiss Path

Menus close three ways: a selection (onSelected), an outside tap/dismiss gesture, or Navigator.pop without a value. onCanceled fires for the second case — use it to roll back optimistic state or close an accompanying overlay, not to log analytics:

PopupMenuButton(
  onSelected: _commitTempSort,
  onCanceled: _revertTempSort,
  itemBuilder: _buildItems,
)

PopupMenuButton vs. PopupMenu vs. MenuAnchor

  • PopupMenuButton — self-contained: anchor + menu + callbacks. Use for app bars and card overflow.
  • PopupMenu — the raw menu surface, for custom positioning or show-on-build flows.
  • MenuAnchor — the modern replacement for complex cases: it supports submenus, keyboard navigation, and custom MenuController opening, and is the direction Material is heading.

If you’re starting fresh on desktop-first apps with nested menus, reach for MenuAnchor; for the classic mobile overflow, PopupMenuButton remains the shortest correct path.

Build items from live state, label your icon-only anchors, and handle onCanceled — and overflow menus behave the way users expect on every platform.