Flutter NavigationDrawer Localization: Material 3 Side Navigation for Multilingual Apps
NavigationDrawer is a Flutter Material 3 widget that provides a slide-out side panel with navigation destinations. In multilingual applications, NavigationDrawer is essential for displaying translated destination labels with icons in a side panel, organizing navigation into localized sections with translated headers, supporting RTL layouts where the drawer slides from the right side, and providing accessible navigation with screen reader announcements in the active language.
Understanding NavigationDrawer in Localization Context
NavigationDrawer renders a Material 3 navigation panel with NavigationDrawerDestination items grouped by optional headers. For multilingual apps, this enables:
- Translated destination labels in a structured side navigation
- Section headers with localized group titles
- RTL-aware drawer that opens from the correct side automatically
- Accessible navigation announcements in the active language
Why NavigationDrawer Matters for Multilingual Apps
NavigationDrawer provides:
- Structured navigation: Translated destinations grouped under localized section headers
- Material 3 design: Updated styling with proper text and icon theming
- Built-in selection: Active destination highlighting with localized labels
- Automatic RTL: Drawer opens from the right in RTL layouts without extra code
Basic NavigationDrawer Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedNavigationDrawerExample extends StatefulWidget {
const LocalizedNavigationDrawerExample({super.key});
@override
State<LocalizedNavigationDrawerExample> createState() =>
_LocalizedNavigationDrawerExampleState();
}
class _LocalizedNavigationDrawerExampleState
extends State<LocalizedNavigationDrawerExample> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appName)),
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
children: [
Padding(
padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
child: Text(
l10n.appName,
style: Theme.of(context).textTheme.titleSmall,
),
),
NavigationDrawerDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: Text(l10n.homeLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.dashboard_outlined),
selectedIcon: const Icon(Icons.dashboard),
label: Text(l10n.dashboardLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.notifications_outlined),
selectedIcon: const Icon(Icons.notifications),
label: Text(l10n.notificationsLabel),
),
const Divider(indent: 28, endIndent: 28),
Padding(
padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
child: Text(
l10n.accountSectionLabel,
style: Theme.of(context).textTheme.titleSmall,
),
),
NavigationDrawerDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
label: Text(l10n.settingsLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.help_outline),
selectedIcon: const Icon(Icons.help),
label: Text(l10n.helpLabel),
),
],
),
body: Center(
child: Text(
l10n.welcomeMessage,
style: Theme.of(context).textTheme.headlineMedium,
),
),
);
}
}
Advanced NavigationDrawer Patterns for Localization
Sectioned Drawer with User Profile
A NavigationDrawer with a localized user profile header and grouped navigation sections.
class ProfileNavigationDrawer extends StatefulWidget {
const ProfileNavigationDrawer({super.key});
@override
State<ProfileNavigationDrawer> createState() =>
_ProfileNavigationDrawerState();
}
class _ProfileNavigationDrawerState extends State<ProfileNavigationDrawer> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appName)),
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
children: [
_DrawerHeader(l10n: l10n),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(28, 16, 16, 10),
child: Text(
l10n.mainSectionLabel,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
NavigationDrawerDestination(
icon: const Icon(Icons.inbox_outlined),
selectedIcon: const Icon(Icons.inbox),
label: Text(l10n.inboxLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.send_outlined),
selectedIcon: const Icon(Icons.send),
label: Text(l10n.sentLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.drafts_outlined),
selectedIcon: const Icon(Icons.drafts),
label: Text(l10n.draftsLabel),
),
const Divider(indent: 28, endIndent: 28),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(28, 16, 16, 10),
child: Text(
l10n.manageSectionLabel,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
NavigationDrawerDestination(
icon: const Icon(Icons.label_outline),
selectedIcon: const Icon(Icons.label),
label: Text(l10n.labelsLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.delete_outline),
selectedIcon: const Icon(Icons.delete),
label: Text(l10n.trashLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.report_outlined),
selectedIcon: const Icon(Icons.report),
label: Text(l10n.spamLabel),
),
],
),
body: const Center(child: Placeholder()),
);
}
}
class _DrawerHeader extends StatelessWidget {
final AppLocalizations l10n;
const _DrawerHeader({required this.l10n});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsetsDirectional.fromSTEB(28, 16, 16, 16),
child: Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
l10n.userInitials,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.displayName,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
l10n.userEmail,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
);
}
}
Modal vs Permanent Drawer Based on Screen Width
An adaptive layout that shows a permanent NavigationDrawer on wide screens and a modal drawer on narrow screens.
class AdaptiveDrawerLayout extends StatefulWidget {
const AdaptiveDrawerLayout({super.key});
@override
State<AdaptiveDrawerLayout> createState() => _AdaptiveDrawerLayoutState();
}
class _AdaptiveDrawerLayoutState extends State<AdaptiveDrawerLayout> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final screenWidth = MediaQuery.sizeOf(context).width;
final useModalDrawer = screenWidth < 800;
final drawerContent = [
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(28, 16, 16, 10),
child: Text(
l10n.navigationLabel,
style: Theme.of(context).textTheme.titleSmall,
),
),
NavigationDrawerDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: Text(l10n.homeLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.explore_outlined),
selectedIcon: const Icon(Icons.explore),
label: Text(l10n.exploreLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.bookmark_outline),
selectedIcon: const Icon(Icons.bookmark),
label: Text(l10n.savedLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
label: Text(l10n.settingsLabel),
),
];
if (useModalDrawer) {
return Scaffold(
appBar: AppBar(title: Text(l10n.appName)),
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
children: drawerContent,
),
body: Center(
child: Text(l10n.contentPlaceholder),
),
);
}
return Scaffold(
body: Row(
children: [
NavigationDrawer(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
children: drawerContent,
),
Expanded(
child: Scaffold(
appBar: AppBar(title: Text(l10n.appName)),
body: Center(
child: Text(l10n.contentPlaceholder),
),
),
),
],
),
);
}
}
Drawer with Badge Counts and Localized Indicators
NavigationDrawer destinations with translated badge labels showing unread counts.
class BadgedNavigationDrawer extends StatefulWidget {
const BadgedNavigationDrawer({super.key});
@override
State<BadgedNavigationDrawer> createState() =>
_BadgedNavigationDrawerState();
}
class _BadgedNavigationDrawerState extends State<BadgedNavigationDrawer> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appName)),
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
children: [
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(28, 16, 16, 10),
child: Text(
l10n.mailSectionLabel,
style: Theme.of(context).textTheme.titleSmall,
),
),
NavigationDrawerDestination(
icon: Badge(
label: Text('24'),
child: const Icon(Icons.inbox_outlined),
),
selectedIcon: Badge(
label: Text('24'),
child: const Icon(Icons.inbox),
),
label: Text(l10n.inboxLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.send_outlined),
selectedIcon: const Icon(Icons.send),
label: Text(l10n.sentLabel),
),
NavigationDrawerDestination(
icon: Badge(
label: Text('3'),
child: const Icon(Icons.drafts_outlined),
),
selectedIcon: Badge(
label: Text('3'),
child: const Icon(Icons.drafts),
),
label: Text(l10n.draftsLabel),
),
const Divider(indent: 28, endIndent: 28),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(28, 16, 16, 10),
child: Text(
l10n.categoriesSectionLabel,
style: Theme.of(context).textTheme.titleSmall,
),
),
NavigationDrawerDestination(
icon: const Icon(Icons.people_outline),
selectedIcon: const Icon(Icons.people),
label: Text(l10n.socialLabel),
),
NavigationDrawerDestination(
icon: Badge(
label: Text('8'),
child: const Icon(Icons.local_offer_outlined),
),
selectedIcon: Badge(
label: Text('8'),
child: const Icon(Icons.local_offer),
),
label: Text(l10n.promotionsLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.info_outline),
selectedIcon: const Icon(Icons.info),
label: Text(l10n.updatesLabel),
),
],
),
body: const Center(child: Placeholder()),
);
}
}
RTL Support and Bidirectional Layouts
NavigationDrawer automatically opens from the right side in RTL layouts. All destination labels and section headers align correctly based on text direction.
class BidirectionalNavigationDrawer extends StatefulWidget {
const BidirectionalNavigationDrawer({super.key});
@override
State<BidirectionalNavigationDrawer> createState() =>
_BidirectionalNavigationDrawerState();
}
class _BidirectionalNavigationDrawerState
extends State<BidirectionalNavigationDrawer> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appName)),
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
children: [
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(28, 16, 16, 10),
child: Text(
l10n.navigationLabel,
style: Theme.of(context).textTheme.titleSmall,
),
),
NavigationDrawerDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: Text(l10n.homeLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.shopping_bag_outlined),
selectedIcon: const Icon(Icons.shopping_bag),
label: Text(l10n.ordersLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.favorite_outline),
selectedIcon: const Icon(Icons.favorite),
label: Text(l10n.wishlistLabel),
),
NavigationDrawerDestination(
icon: const Icon(Icons.person_outline),
selectedIcon: const Icon(Icons.person),
label: Text(l10n.accountLabel),
),
],
),
body: Center(
child: Text(l10n.welcomeMessage),
),
);
}
}
Testing NavigationDrawer 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 LocalizedNavigationDrawerExample(),
);
}
testWidgets('NavigationDrawer renders localized destinations',
(tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
// Open the drawer
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
expect(find.byType(NavigationDrawer), findsOneWidget);
});
testWidgets('NavigationDrawer works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
}
Best Practices
Use
EdgeInsetsDirectionalfor all padding in drawer headers and section labels so spacing adapts correctly in RTL layouts.Group destinations with
Dividerand section headers using translated labels to organize complex navigation hierarchies.Provide
selectedIconalongsideiconfor each destination to give clear visual feedback of the active selection.Switch between modal and permanent drawer based on screen width, using the same translated content for both modes.
Use
Badgeon destination icons to show unread counts, keeping badge labels short since space is limited inside drawer icons.Test drawer opening direction in RTL locales to verify it slides from the right side and all labels align correctly.
Conclusion
NavigationDrawer provides a Material 3 side navigation panel for Flutter apps. For multilingual apps, it handles translated destination labels and section headers, supports automatic RTL drawer direction, and integrates badge counts for unread indicators. By combining NavigationDrawer with user profile headers, adaptive modal/permanent layouts, and grouped sections, you can build navigation experiences that organize translated content clearly across all supported languages.