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
Use
leadingIcononMenuItemButtonto provide visual context alongside translated labels, helping users identify actions across languages.Group related items with
Dividerto visually separate translated menu sections.Use
SubmenuButtonfor cascading submenus with translated group labels to organize complex menu hierarchies.Support both right-click and long-press with
onSecondaryTapandonLongPressto trigger context menus on both desktop and mobile.Display keyboard shortcuts via
trailingIconalongside translated action labels for desktop-oriented menus.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.