Skip to content
Boring Doc
BreakingHigh impactOfficial migration guidesince 3.7.0landed 3.6.0-0.0.pre

Replaced parameters for customizing context menus with a generic widget builder

Framework · Official source

Context menus, or text selection toolbars, are the menus that show up when long pressing or right clicking on text in Flutter, and they show options like **Cut**, **Copy**, **Paste**, and **Select all**. Previously, it was only possible to narrowly customize them using `ToolbarOptions` and `TextSelectionControls`. Now, they have been made composable using widgets, just like everything else in Flutter, and the specific configuration parameters have been deprecated.

Original behavior

Previously, it was possible to disable buttons from the context menus using `TextSelectionControls`, but any customization beyond that required copying and editing hundreds of lines of custom classes in the framework. Now, all of this has been replaced by a simple builder function, `contextMenuBuilder`, which allows any Flutter widget to be used as a context menu.

New behavior

Context menus are now built from the `contextMenuBuilder` parameter, which has been added to all text-editing and text-selection widgets. If one is not provided, then Flutter just sets it to a default that builds the correct context menu for the given platform. All of these default widgets are exposed to users for re-use. Customizing context menus now consists of using `contextMenuBuilder` to return whatever widget you want, possibly including reusing the built-in context menu widgets.

Who is affected
Applications and packages relying on the previous behaviour or signature. Code that uses ToolbarOptions, TextSelectionControls, TextField and related APIs.
Practical impact
Behaviour or API differences that can break builds or change runtime behaviour.
What action should I take?
Follow the official migration guide below, update affected call sites, run `dart fix --apply` where a fix is available, then run your tests.
Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)

In general, any previous changes to context menus that have been deprecated now require the use of the contextMenuBuilder parameter on the relevant text-editing or text-selection widget ( on TextField, for example). Return a built-in context menu widget like AdaptiveTextSelectionToolbar to use Flutter's built-in context menus, or return your own widget for something totally custom.

To transition to contextMenuBuilder, the following parameters and classes have been deprecated.

ToolbarOptions

This class was previously used to explicitly enable or disable certain buttons in a context menu. Before this change, you might have passed it into TextField or other widgets like this:

// Deprecated.
TextField(
  toolbarOptions: ToolbarOptions(
    copy: true,
  ),
)

Now, you can achieve the same effect by adjusting the buttonItems passed into AdaptiveTextSelectionToolbar. For example, you could ensure that the Cut button never appears, but the other buttons do appear as usual:

TextField(
  contextMenuBuilder: (context, editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    buttonItems.removeWhere((ContextMenuButtonItem buttonItem) {
      return buttonItem.type == ContextMenuButtonType.cut;
    });
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

Or, you could ensure that the Cut button appears exclusively and always:

TextField(
  contextMenuBuilder: (context, editableTextState) {
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: <ContextMenuButtonItem>[
        ContextMenuButtonItem(
          onPressed: () {
            editableTextState.cutSelection(SelectionChangedCause.toolbar);
          },
          type: ContextMenuButtonType.cut,
        ),
      ],
    );
  },
)

TextSelectionControls.canCut and other button booleans

These booleans previously had the same effect of enabling and disabling certain buttons as ToolbarOptions.cut, and so on had. Before this change, you might have been hiding and showing buttons by overriding TextSelectionControls and setting these booleans like this:

// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool canCut() => false,
}

See the previous section on ToolbarOptions for how to achieve a similar effect with contextMenuBuilder.

TextSelectionControls.handleCut and other button callbacks

These functions allowed the modification of the callback called when the buttons were pressed. Before this change, you might have been modifying context menu button callbacks by overriding these handler methods like this:

// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool handleCut() {
    // My custom cut implementation here.
  },
}

This is still possible using contextMenuBuilder, including calling out to the original buttons' actions in the custom handler, using toolbar widgets like AdaptiveTextSelectionToolbar.buttonItems.

This example shows modifying the Copy button to show a dialog in addition to doing its usual copy logic.

TextField(
  contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    final int copyButtonIndex = buttonItems.indexWhere(
      (ContextMenuButtonItem buttonItem) {
        return buttonItem.type == ContextMenuButtonType.copy;
      },
    );
    if (copyButtonIndex >= 0) {
      final ContextMenuButtonItem copyButtonItem =
          buttonItems[copyButtonIndex];
      buttonItems[copyButtonIndex] = copyButtonItem.copyWith(
        onPressed: () {
          copyButtonItem.onPressed();
          Navigator.of(context).push(
            DialogRoute<void>(
              context: context,
              builder: (BuildContext context) =>
                const AlertDialog(
                  title: Text('Copied, but also showed this dialog.'),
                ),
            );
          )
        },
      );
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

A full example of modifying a built-in context menu action can be found in the samples repository in modified_action_page.dart on GitHub.

buildToolbar

This function generated the context menu widget similarly to contextMenuBuilder, but required more setup to use. Before this change, you might have been overriding buildToolbar as a part of TextSelectionControls, like this:

// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
    double textLineHeight,
    Offset selectionMidpoint,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
    ClipboardStatusNotifier clipboardStatus,
    Offset lastSecondaryTapDownPosition,
  ) {
    return _MyCustomToolbar();
  },
}

Now you can simply use contextMenuBuilder directly as a parameter to TextField (and others). The information provided in the parameters to buildToolbar can be obtained from the EditableTextState that is passed to contextMenuBuilder.

The following example shows how to build a fully-custom toolbar from scratch while still using the default buttons.

class _MyContextMenu extends StatelessWidget {
  const _MyContextMenu({
    required this.anchor,
    required this.children,
  });

  final Offset anchor;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: anchor.dy,
          left: anchor.dx,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.amberAccent,
            child: Column(
              children: children,
            ),
          ),
        ),
      ],
    );
  }
}

class _MyTextField extends StatelessWidget {
  const _MyTextField();

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      maxLines: 4,
      minLines: 2,
      contextMenuBuilder: (context, editableTextState) {
        return _MyContextMenu(
          anchor: editableTextState.contextMenuAnchors.primaryAnchor,
          children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
            context,
            editableTextState.contextMenuButtonItems,
          ).toList(),
        );
      },
    );
  }
}

A full example of building a custom context menu can be found in the samples repository in custom_menu_page.dart on GitHub.

Migration assistantAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.
Official migration guide·Verified classification (95%)

Listed in the official breaking-changes index (docs.flutter.dev/release/breaking-changes).

An official announcement or migration guide documents an intended behaviour or API change. A verified source change (git diff) shows code changed but does not by itself prove a public API is breaking. AI interpretations are labelled and never used to classify a change.

Source commit
b3045439b7c0
Source last updated
Oct 21, 2026
Last synchronized
Oct 21, 2026
Publication status
published
Publication history
  • Oct 21, 2026, 8:31 PM: new → published (ingestion) — Backed by an official breaking-change/migration guide.
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.
Was this useful?