In a Flutter desktop app, how do you open a context menu at the pointer when the user right-clicks an item?
answer
- secondary button means right-click
- position from the tap details
- MenuAnchor with a MenuController
- open(position:) in anchor coordinates
- text fields already have one
basics
~10 sWrap 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 sA 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 linesimport '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
Recall that right-click is a secondary tap and that MenuAnchor with menu items shows the menu.
Explain opening the menu at the pointer with MenuController.open(position:) in the anchor's coordinates, and what text fields already provide.
Show consistent action wiring between context menus, toolbars and shortcuts, plus long-press parity when the widget also ships on mobile.
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.