← Back to Blog

Flutter MenuAnchor Localization: Context Menus for Multilingual Apps

fluttermenuanchormaterial3menulocalizationrtl

Flutter MenuAnchor Localization: Context Menus for Multilingual Apps

MenuAnchor is a Flutter Material 3 widget that attaches a menu to a child widget, displaying it when triggered. In multilingual applications, MenuAnchor is essential for building context menus with translated menu items, creating cascading submenus with localized labels, supporting RTL menu alignment and text direction, and providing accessible menu navigation with announcements in the active language.

Understanding MenuAnchor in Localization Context

MenuAnchor renders a Material 3 menu attached to an anchor widget, supporting nested submenus and keyboard navigation. For multilingual apps, this enables:

  • Translated menu items with optional icons and keyboard shortcuts
  • Cascading submenus with localized group labels
  • RTL-aware menu positioning and text alignment
  • Accessible menu navigation with translated item descriptions

Why MenuAnchor Matters for Multilingual Apps

MenuAnchor provides:

  • Material 3 menus: Updated styling with translated menu items and dividers
  • Cascading submenus: Nested menus with localized group labels
  • Keyboard shortcuts: Displayed alongside translated labels
  • Flexible anchoring: Attach menus to any widget with proper RTL positioning

Basic MenuAnchor Implementation

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedMenuAnchorExample extends StatelessWidget {
  const LocalizedMenuAnchorExample({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.documentTitle),
        actions: [
          MenuAnchor(
            menuChildren: [
              MenuItemButton(
                leadingIcon: const Icon(Icons.content_copy),
                child: Text(l10n.copyAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.content_paste),
                child: Text(l10n.pasteAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.select_all),
                child: Text(l10n.selectAllAction),
                onPressed: () {},
              ),
              const Divider(),
              MenuItemButton(
                leadingIcon: const Icon(Icons.share),
                child: Text(l10n.shareAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.delete_outline),
                child: Text(l10n.deleteAction),
                onPressed: () {},
              ),
            ],
            builder: (context, controller, child) {
              return IconButton(
                icon: const Icon(Icons.more_vert),
                tooltip: l10n.moreOptionsTooltip,
                onPressed: () {
                  if (controller.isOpen) {
                    controller.close();
                  } else {
                    controller.open();
                  }
                },
              );
            },
          ),
        ],
      ),
      body: Center(
        child: Text(l10n.documentContentPlaceholder),
      ),
    );
  }
}

Advanced MenuAnchor Patterns for Localization

Cascading Submenus with Localized Groups

MenuAnchor with nested submenus for organized translated menu items.

class CascadingLocalizedMenu extends StatelessWidget {
  const CascadingLocalizedMenu({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.fileManagerTitle),
        actions: [
          MenuAnchor(
            menuChildren: [
              MenuItemButton(
                leadingIcon: const Icon(Icons.create_new_folder),
                child: Text(l10n.newFolderAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.upload_file),
                child: Text(l10n.uploadFileAction),
                onPressed: () {},
              ),
              const Divider(),
              SubmenuButton(
                leadingIcon: const Icon(Icons.sort),
                menuChildren: [
                  MenuItemButton(
                    leadingIcon: const Icon(Icons.sort_by_alpha),
                    child: Text(l10n.sortByNameLabel),
                    onPressed: () {},
                  ),
                  MenuItemButton(
                    leadingIcon: const Icon(Icons.calendar_today),
                    child: Text(l10n.sortByDateLabel),
                    onPressed: () {},
                  ),
                  MenuItemButton(
                    leadingIcon: const Icon(Icons.data_usage),
                    child: Text(l10n.sortBySizeLabel),
                    onPressed: () {},
                  ),
                  MenuItemButton(
                    leadingIcon: const Icon(Icons.category),
                    child: Text(l10n.sortByTypeLabel),
                    onPressed: () {},
                  ),
                ],
                child: Text(l10n.sortByLabel),
              ),
              SubmenuButton(
                leadingIcon: const Icon(Icons.view_module),
                menuChildren: [
                  MenuItemButton(
                    leadingIcon: const Icon(Icons.grid_view),
                    child: Text(l10n.gridViewLabel),
                    onPressed: () {},
                  ),
                  MenuItemButton(
                    leadingIcon: const Icon(Icons.list),
                    child: Text(l10n.listViewLabel),
                    onPressed: () {},
                  ),
                  MenuItemButton(
                    leadingIcon: const Icon(Icons.view_compact),
                    child: Text(l10n.compactViewLabel),
                    onPressed: () {},
                  ),
                ],
                child: Text(l10n.viewAsLabel),
              ),
              const Divider(),
              MenuItemButton(
                leadingIcon: const Icon(Icons.settings),
                child: Text(l10n.settingsLabel),
                onPressed: () {},
              ),
            ],
            builder: (context, controller, child) {
              return IconButton(
                icon: const Icon(Icons.more_vert),
                tooltip: l10n.menuTooltip,
                onPressed: () {
                  if (controller.isOpen) {
                    controller.close();
                  } else {
                    controller.open();
                  }
                },
              );
            },
          ),
        ],
      ),
      body: const Center(child: Placeholder()),
    );
  }
}

Right-Click Context Menu

MenuAnchor triggered by secondary tap (right-click) with translated context actions.

class ContextMenuExample extends StatelessWidget {
  const ContextMenuExample({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: Text(l10n.textEditorTitle)),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 10,
        itemBuilder: (context, index) {
          return MenuAnchor(
            menuChildren: [
              MenuItemButton(
                leadingIcon: const Icon(Icons.edit),
                child: Text(l10n.editAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.content_copy),
                child: Text(l10n.duplicateAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.drive_file_move),
                child: Text(l10n.moveAction),
                onPressed: () {},
              ),
              const Divider(),
              MenuItemButton(
                leadingIcon: const Icon(Icons.archive),
                child: Text(l10n.archiveAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: Icon(
                  Icons.delete,
                  color: Theme.of(context).colorScheme.error,
                ),
                child: Text(
                  l10n.deleteAction,
                  style: TextStyle(
                    color: Theme.of(context).colorScheme.error,
                  ),
                ),
                onPressed: () {},
              ),
            ],
            builder: (context, controller, child) {
              return GestureDetector(
                onSecondaryTap: () => controller.open(),
                onLongPress: () => controller.open(),
                child: Card(
                  child: ListTile(
                    leading: const Icon(Icons.description),
                    title: Text('${l10n.documentLabel} ${index + 1}'),
                    subtitle: Text(l10n.documentDescription),
                    trailing: IconButton(
                      icon: const Icon(Icons.more_vert),
                      tooltip: l10n.moreOptionsTooltip,
                      onPressed: () => controller.open(),
                    ),
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Menu with Keyboard Shortcuts Display

MenuAnchor items showing translated labels alongside keyboard shortcut hints.

class ShortcutMenuExample extends StatelessWidget {
  const ShortcutMenuExample({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.editorTitle),
        actions: [
          MenuAnchor(
            menuChildren: [
              MenuItemButton(
                leadingIcon: const Icon(Icons.file_open),
                trailingIcon: Text(
                  '⌘O',
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                ),
                child: Text(l10n.openFileAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.save),
                trailingIcon: Text(
                  '⌘S',
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                ),
                child: Text(l10n.saveAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.save_as),
                trailingIcon: Text(
                  '⇧⌘S',
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                ),
                child: Text(l10n.saveAsAction),
                onPressed: () {},
              ),
              const Divider(),
              MenuItemButton(
                leadingIcon: const Icon(Icons.undo),
                trailingIcon: Text(
                  '⌘Z',
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                ),
                child: Text(l10n.undoAction),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.redo),
                trailingIcon: Text(
                  '⇧⌘Z',
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                ),
                child: Text(l10n.redoAction),
                onPressed: () {},
              ),
            ],
            builder: (context, controller, child) {
              return TextButton(
                onPressed: () {
                  if (controller.isOpen) {
                    controller.close();
                  } else {
                    controller.open();
                  }
                },
                child: Text(l10n.fileMenuLabel),
              );
            },
          ),
        ],
      ),
      body: const Center(child: Placeholder()),
    );
  }
}

RTL Support and Bidirectional Layouts

MenuAnchor automatically positions menus correctly in RTL layouts. Menu items align text from right to left, and leading/trailing icons swap sides.

class BidirectionalMenuAnchor extends StatelessWidget {
  const BidirectionalMenuAnchor({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.settingsTitle),
        actions: [
          MenuAnchor(
            menuChildren: [
              MenuItemButton(
                leadingIcon: const Icon(Icons.language),
                child: Text(l10n.languageLabel),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.palette),
                child: Text(l10n.themeLabel),
                onPressed: () {},
              ),
              MenuItemButton(
                leadingIcon: const Icon(Icons.text_fields),
                child: Text(l10n.fontSizeLabel),
                onPressed: () {},
              ),
              const Divider(),
              MenuItemButton(
                leadingIcon: const Icon(Icons.info_outline),
                child: Text(l10n.aboutLabel),
                onPressed: () {},
              ),
            ],
            builder: (context, controller, child) {
              return IconButton(
                icon: const Icon(Icons.more_vert),
                tooltip: l10n.optionsTooltip,
                onPressed: () {
                  if (controller.isOpen) {
                    controller.close();
                  } else {
                    controller.open();
                  }
                },
              );
            },
          ),
        ],
      ),
      body: const Center(child: Placeholder()),
    );
  }
}

Testing MenuAnchor Localization

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() {
  Widget buildTestWidget({Locale locale = const Locale('en')}) {
    return MaterialApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedMenuAnchorExample(),
    );
  }

  testWidgets('MenuAnchor renders and opens', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    await tester.tap(find.byIcon(Icons.more_vert));
    await tester.pumpAndSettle();
    expect(find.byType(MenuItemButton), findsWidgets);
  });

  testWidgets('MenuAnchor works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Use leadingIcon on MenuItemButton to provide visual context alongside translated labels, helping users identify actions across languages.

  2. Group related items with Divider to visually separate translated menu sections.

  3. Use SubmenuButton for cascading submenus with translated group labels to organize complex menu hierarchies.

  4. Support both right-click and long-press with onSecondaryTap and onLongPress to trigger context menus on both desktop and mobile.

  5. Display keyboard shortcuts via trailingIcon alongside translated action labels for desktop-oriented menus.

  6. Test menu positioning in RTL to verify menus open on the correct side and text aligns properly.

Conclusion

MenuAnchor provides a Material 3 context menu system for Flutter apps. For multilingual apps, it handles translated menu items with icons, supports cascading submenus with localized groups, and automatically adapts positioning for RTL layouts. By combining MenuAnchor with context menus, file manager menus, and keyboard shortcut displays, you can build rich menu interfaces that work naturally in every supported language.

Further Reading