skip to content

In a Flutter desktop app, how do you open a context menu at the pointer when the user right-clicks an item?

level: middleimportance: should knowfreq 26%

answer

  1. secondary button means right-click
  2. position from the tap details
  3. MenuAnchor with a MenuController
  4. open(position:) in anchor coordinates
  5. text fields already have one

basics

~10 s

Wrap the item in a MenuAnchor with menuChildren, catch the right-click with GestureDetector.onSecondaryTapUp, and call MenuController.open(position: details.localPosition), which places the menu at that point in the anchor's coordinates.

solid answer

~40 s

A right-click is a **secondary** tap, which `GestureDetector` reports through `onSecondaryTapDown` and `onSecondaryTapUp`. Flutter's Material menu system provides the menu itself: a `MenuAnchor` with a `MenuController` and `menuChildren` such as `MenuItemButton`s. In `onSecondaryTapUp`, call `controller.open(position: details.localPosition)`; `open` takes a position in the anchor's own coordinate space and overrides `alignmentOffset`, so the menu appears under the pointer. The menu closes on selection, on an outside tap, or if the anchor scrolls. Text fields and selectable text already show a platform-appropriate right-click menu through their `contextMenuBuilder`, so you only build one for your own items. On the web, the browser's own menu also appears unless it is disabled.

code

dart · 37 lines
dart
import 'package:flutter/material.dart';

class InvoiceContextMenu extends StatefulWidget {
  const InvoiceContextMenu({
    super.key,
    required this.child,
    required this.onDuplicate,
    required this.onMarkPaid,
  });

  final Widget child;
  final VoidCallback onDuplicate;
  final VoidCallback onMarkPaid;

  @override
  State<InvoiceContextMenu> createState() => _InvoiceContextMenuState();
}

class _InvoiceContextMenuState extends State<InvoiceContextMenu> {
  final MenuController _menu = MenuController();

  @override
  Widget build(BuildContext context) {
    return MenuAnchor(
      controller: _menu,
      menuChildren: <Widget>[
        MenuItemButton(onPressed: widget.onDuplicate, child: const Text('Duplicate')),
        MenuItemButton(onPressed: widget.onMarkPaid, child: const Text('Mark as paid')),
      ],
      child: GestureDetector(
        onSecondaryTapUp: (TapUpDetails details) =>
            _menu.open(position: details.localPosition),
        child: widget.child,
      ),
    );
  }
}

go deeper

for a junior

Recall that right-click is a secondary tap and that MenuAnchor with menu items shows the menu.

for a middle

Explain opening the menu at the pointer with MenuController.open(position:) in the anchor's coordinates, and what text fields already provide.

for a senior

Show consistent action wiring between context menus, toolbars and shortcuts, plus long-press parity when the widget also ships on mobile.

for a principal

Decide how far the product adopts desktop interaction conventions such as context menus and whether they are required on every platform build.

## What desktop users expect On Windows, macOS and Linux, right-clicking an item opens a small menu of actions for that item: open, duplicate, delete. A Flutter app built for touch has no such behaviour, because a touch screen has no secondary button. Adding it is one of the first things that makes a desktop build feel native. ## The pieces | Piece | Role | |---|---| | `GestureDetector.onSecondaryTapUp` | detects the right-click and supplies `TapUpDetails` with `localPosition` and `globalPosition` | | `MenuAnchor` | owns the menu overlay and the widget it is anchored to | | `MenuController` | opens and closes the menu from code | | `MenuItemButton` / `SubmenuButton` | the entries, with optional `shortcut` labels and nested menus | `MenuController.open({Offset? position})` interprets `position` in the **coordinate space of the anchor**, and a given position overrides the anchor's `alignmentOffset`. So `details.localPosition` from a gesture detector that wraps the same area puts the menu exactly at the pointer. ## Wiring it up 1. Create a `MenuController` in the item's `State`. 2. Build a `MenuAnchor(controller: ..., menuChildren: [...], child: ...)` around the item. 3. Inside it, wrap the item in a `GestureDetector` whose `onSecondaryTapUp` calls `controller.open(position: details.localPosition)`. 4. Give each `MenuItemButton` an `onPressed`; the menu closes after a selection. 5. Optionally close an already open menu on a primary tap elsewhere; `MenuAnchor` closes itself on outside taps by default. The menu also closes automatically if its anchor is scrolled by an ancestor or the view changes size, which avoids a menu floating over the wrong row after a list scrolls. ## What you do not need to build - **Text fields** and **selectable text** already show a right-click menu with cut, copy, paste and select all, built by their `contextMenuBuilder` and adapted to the platform. Override `contextMenuBuilder` only to add or remove entries. - **Keyboard access**: menu items are focusable and keyboard-navigable once open; assigning global shortcuts to the same actions is a separate concern handled by shortcut and action widgets. ## Platform notes - On **macOS** trackpads, a two-finger click is the system's secondary click, so the same handler serves trackpad users. - On the **web**, the browser shows its own context menu too; Flutter's `BrowserContextMenu` can disable it for the page. - Long-press is the touch equivalent; if the same widget ships on mobile, call the same menu-opening code from `onLongPressStart`. ## Common mistakes - Passing `globalPosition` to `open(position:)`, which places the menu offset by the row's position in the window. - Pushing a dialog route to imitate a context menu, which loses keyboard navigation and the close-on-outside-tap behaviour. - Creating one `MenuController` per list and reusing it for every row, so the menu opens anchored to the wrong item. - Rebuilding a custom menu for text fields that already have a platform-correct one. ## Worked example In a small-shop invoice tool, right-clicking an invoice row offers "Open", "Duplicate" and "Mark as paid". Each row owns its `MenuController`, opens the menu at the pointer from `onSecondaryTapUp`, and routes each item to the same actions the toolbar uses, so behaviour stays consistent.

  • Why pass details.localPosition rather than globalPosition to MenuController.open in Flutter?
    `open(position:)` expects a position in the anchor's coordinate space. When the `GestureDetector` covers the anchor's child, `localPosition` is already relative to that area, so the menu opens under the pointer. `globalPosition` is relative to the whole view and would place the menu offset by the row's position on screen.
  • The invoice notes field is a TextField. Should the app build its own right-click menu for it?
    Usually not. `TextField` already shows a platform-adapted menu with cut, copy, paste and select all through its default `contextMenuBuilder`. Override `contextMenuBuilder` only to add or remove entries, so the field keeps the platform's behaviour.

saying these in an interview costs you the question

  • Flutter has no way to detect a right-click.
  • MenuController.open takes global screen coordinates.
  • Every TextField needs a hand-built right-click menu.
  • A right-click menu must be a new route pushed on the Navigator.