Flutter DropdownMenu Localization: Material 3 Menus for Multilingual Apps
DropdownMenu is a Flutter Material 3 widget that displays a text field with an attached dropdown list of selectable options. In multilingual applications, DropdownMenu is essential for presenting translated menu items with search/filter support, handling variable-width menu options across different languages, supporting RTL text input and menu alignment, and providing accessible dropdown interactions with screen reader announcements in the active language.
Understanding DropdownMenu in Localization Context
DropdownMenu renders a Material 3 dropdown with a filterable text field and DropdownMenuEntry items. For multilingual apps, this enables:
- Translated menu entries with optional leading icons
- Built-in text filtering that works with any language
- RTL-aware text field and menu positioning
- Localized helper text, labels, and error messages
Why DropdownMenu Matters for Multilingual Apps
DropdownMenu provides:
- Search/filter support: Users can type to filter translated options in any language
- Material 3 design: Updated styling with proper text field integration
- Width flexibility: Menu width adapts to the longest translated option or a fixed width
- Input decoration: Full
InputDecorationsupport for translated labels, hints, and errors
Basic DropdownMenu Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedDropdownMenuExample extends StatefulWidget {
const LocalizedDropdownMenuExample({super.key});
@override
State<LocalizedDropdownMenuExample> createState() =>
_LocalizedDropdownMenuExampleState();
}
class _LocalizedDropdownMenuExampleState
extends State<LocalizedDropdownMenuExample> {
String? _selectedCountry;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.selectCountryTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownMenu<String>(
label: Text(l10n.countryLabel),
hintText: l10n.selectCountryHint,
expandedInsets: EdgeInsets.zero,
onSelected: (value) {
setState(() => _selectedCountry = value);
},
dropdownMenuEntries: [
DropdownMenuEntry(
value: 'us',
label: l10n.unitedStatesLabel,
leadingIcon: const Text('🇺🇸'),
),
DropdownMenuEntry(
value: 'uk',
label: l10n.unitedKingdomLabel,
leadingIcon: const Text('🇬🇧'),
),
DropdownMenuEntry(
value: 'de',
label: l10n.germanyLabel,
leadingIcon: const Text('🇩🇪'),
),
DropdownMenuEntry(
value: 'fr',
label: l10n.franceLabel,
leadingIcon: const Text('🇫🇷'),
),
DropdownMenuEntry(
value: 'jp',
label: l10n.japanLabel,
leadingIcon: const Text('🇯🇵'),
),
DropdownMenuEntry(
value: 'sa',
label: l10n.saudiArabiaLabel,
leadingIcon: const Text('🇸🇦'),
),
],
),
if (_selectedCountry != null) ...[
const SizedBox(height: 16),
Text(
l10n.selectedCountryMessage(_selectedCountry!),
style: Theme.of(context).textTheme.bodyLarge,
),
],
],
),
),
);
}
}
Advanced DropdownMenu Patterns for Localization
Language Selector with DropdownMenu
A locale picker that uses DropdownMenu with translated language names and filters.
class LocaleDropdownMenu extends StatefulWidget {
final ValueChanged<Locale> onLocaleChanged;
const LocaleDropdownMenu({
super.key,
required this.onLocaleChanged,
});
@override
State<LocaleDropdownMenu> createState() => _LocaleDropdownMenuState();
}
class _LocaleDropdownMenuState extends State<LocaleDropdownMenu> {
Locale? _selectedLocale;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final currentLocale = Localizations.localeOf(context);
final locales = [
(const Locale('en'), l10n.englishLanguage, 'English'),
(const Locale('ar'), l10n.arabicLanguage, 'العربية'),
(const Locale('de'), l10n.germanLanguage, 'Deutsch'),
(const Locale('es'), l10n.spanishLanguage, 'Español'),
(const Locale('fr'), l10n.frenchLanguage, 'Français'),
(const Locale('ja'), l10n.japaneseLanguage, '日本語'),
(const Locale('zh'), l10n.chineseLanguage, '中文'),
];
return DropdownMenu<Locale>(
label: Text(l10n.languageLabel),
hintText: l10n.selectLanguageHint,
initialSelection: currentLocale,
expandedInsets: EdgeInsets.zero,
onSelected: (locale) {
if (locale != null) {
setState(() => _selectedLocale = locale);
widget.onLocaleChanged(locale);
}
},
dropdownMenuEntries: locales.map((entry) {
final (locale, translatedName, nativeName) = entry;
return DropdownMenuEntry<Locale>(
value: locale,
label: translatedName,
trailingIcon: Text(
nativeName,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}).toList(),
);
}
}
Form with Multiple Translated Dropdowns
A settings form with multiple DropdownMenu fields using translated options.
class LocalizedSettingsForm extends StatefulWidget {
const LocalizedSettingsForm({super.key});
@override
State<LocalizedSettingsForm> createState() => _LocalizedSettingsFormState();
}
class _LocalizedSettingsFormState extends State<LocalizedSettingsForm> {
String? _theme;
String? _fontSize;
String? _dateFormat;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.displaySettingsTitle)),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.appearanceSectionLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
DropdownMenu<String>(
label: Text(l10n.themeLabel),
hintText: l10n.selectThemeHint,
expandedInsets: EdgeInsets.zero,
initialSelection: _theme,
onSelected: (value) => setState(() => _theme = value),
dropdownMenuEntries: [
DropdownMenuEntry(
value: 'system',
label: l10n.systemThemeLabel,
leadingIcon: const Icon(Icons.brightness_auto),
),
DropdownMenuEntry(
value: 'light',
label: l10n.lightThemeLabel,
leadingIcon: const Icon(Icons.light_mode),
),
DropdownMenuEntry(
value: 'dark',
label: l10n.darkThemeLabel,
leadingIcon: const Icon(Icons.dark_mode),
),
],
),
const SizedBox(height: 16),
DropdownMenu<String>(
label: Text(l10n.fontSizeLabel),
hintText: l10n.selectFontSizeHint,
expandedInsets: EdgeInsets.zero,
initialSelection: _fontSize,
onSelected: (value) => setState(() => _fontSize = value),
dropdownMenuEntries: [
DropdownMenuEntry(
value: 'small',
label: l10n.smallLabel,
),
DropdownMenuEntry(
value: 'medium',
label: l10n.mediumLabel,
),
DropdownMenuEntry(
value: 'large',
label: l10n.largeLabel,
),
DropdownMenuEntry(
value: 'extra_large',
label: l10n.extraLargeLabel,
),
],
),
const SizedBox(height: 24),
Text(
l10n.regionSectionLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
DropdownMenu<String>(
label: Text(l10n.dateFormatLabel),
hintText: l10n.selectDateFormatHint,
expandedInsets: EdgeInsets.zero,
initialSelection: _dateFormat,
onSelected: (value) => setState(() => _dateFormat = value),
dropdownMenuEntries: [
DropdownMenuEntry(
value: 'mdy',
label: l10n.dateFormatMDY,
),
DropdownMenuEntry(
value: 'dmy',
label: l10n.dateFormatDMY,
),
DropdownMenuEntry(
value: 'ymd',
label: l10n.dateFormatYMD,
),
],
),
],
),
),
);
}
}
Filterable Dropdown with Translated Entries
A DropdownMenu with enableFilter that lets users type to search through translated options.
class FilterableLocalizedDropdown extends StatelessWidget {
const FilterableLocalizedDropdown({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final categories = [
(Icons.restaurant, l10n.foodCategoryLabel),
(Icons.local_cafe, l10n.drinksCategoryLabel),
(Icons.shopping_bag, l10n.shoppingCategoryLabel),
(Icons.directions_car, l10n.transportCategoryLabel),
(Icons.movie, l10n.entertainmentCategoryLabel),
(Icons.fitness_center, l10n.healthCategoryLabel),
(Icons.school, l10n.educationCategoryLabel),
(Icons.home, l10n.housingCategoryLabel),
(Icons.work, l10n.incomeCategoryLabel),
(Icons.more_horiz, l10n.otherCategoryLabel),
];
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.addExpenseTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
DropdownMenu<String>(
label: Text(l10n.categoryLabel),
hintText: l10n.searchCategoryHint,
expandedInsets: EdgeInsets.zero,
enableFilter: true,
requestFocusOnTap: true,
dropdownMenuEntries: categories.asMap().entries.map((entry) {
final (icon, label) = categories[entry.key];
return DropdownMenuEntry<String>(
value: label,
label: label,
leadingIcon: Icon(icon),
);
}).toList(),
),
const SizedBox(height: 16),
Text(
l10n.typeToFilterHint,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}
RTL Support and Bidirectional Layouts
DropdownMenu automatically handles RTL text input, menu alignment, and icon positioning. The text field and dropdown list both respect the active text direction.
class BidirectionalDropdownMenu extends StatelessWidget {
const BidirectionalDropdownMenu({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.settingsTitle)),
body: Padding(
padding: const EdgeInsetsDirectional.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownMenu<String>(
label: Text(l10n.textDirectionLabel),
expandedInsets: EdgeInsets.zero,
dropdownMenuEntries: [
DropdownMenuEntry(
value: 'auto',
label: l10n.autoDirectionLabel,
),
DropdownMenuEntry(
value: 'ltr',
label: l10n.leftToRightLabel,
),
DropdownMenuEntry(
value: 'rtl',
label: l10n.rightToLeftLabel,
),
],
),
const SizedBox(height: 16),
DropdownMenu<String>(
label: Text(l10n.currencyLabel),
expandedInsets: EdgeInsets.zero,
dropdownMenuEntries: [
DropdownMenuEntry(
value: 'usd',
label: l10n.usDollarLabel,
),
DropdownMenuEntry(
value: 'eur',
label: l10n.euroLabel,
),
DropdownMenuEntry(
value: 'sar',
label: l10n.saudiRiyalLabel,
),
DropdownMenuEntry(
value: 'irr',
label: l10n.iranianRialLabel,
),
],
),
],
),
),
);
}
}
Testing DropdownMenu 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 LocalizedDropdownMenuExample(),
);
}
testWidgets('DropdownMenu renders localized entries', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(DropdownMenu<String>), findsOneWidget);
});
testWidgets('DropdownMenu works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('DropdownMenu opens and shows entries', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
await tester.tap(find.byType(DropdownMenu<String>));
await tester.pumpAndSettle();
expect(find.byType(DropdownMenuEntry<String>), findsWidgets);
});
}
Best Practices
Use
expandedInsets: EdgeInsets.zeroto make the dropdown menu match the width of its parent, ensuring consistent layout across translations of different lengths.Enable
enableFilterfor dropdown menus with many options so users can type to search through translated entries in any language.Provide
leadingIconon entries to give visual context alongside translated labels, helping users identify options without relying on text alone.Use
hintTextwith translated placeholder text to guide users on what to select before they interact with the dropdown.Set
initialSelectionto a sensible default based on the user's locale, such as pre-selecting their country or preferred language.Test filter behavior with non-Latin scripts to ensure the text filtering works correctly with Arabic, CJK, and other scripts used in your supported locales.
Conclusion
DropdownMenu provides a Material 3 dropdown with built-in text filtering for Flutter apps. For multilingual apps, it handles translated menu entries with search support in any language, provides full InputDecoration for localized labels and hints, and automatically adapts to RTL text input and menu positioning. By combining DropdownMenu with locale selectors, settings forms, and filterable category pickers, you can build dropdown interactions that feel native in every supported language.