← Back to Blog

Flutter NavigationDrawer Localization: Material 3 Side Navigation for Multilingual Apps

flutternavigationdrawermaterial3navigationlocalizationrtl

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

  1. Use EdgeInsetsDirectional for all padding in drawer headers and section labels so spacing adapts correctly in RTL layouts.

  2. Group destinations with Divider and section headers using translated labels to organize complex navigation hierarchies.

  3. Provide selectedIcon alongside icon for each destination to give clear visual feedback of the active selection.

  4. Switch between modal and permanent drawer based on screen width, using the same translated content for both modes.

  5. Use Badge on destination icons to show unread counts, keeping badge labels short since space is limited inside drawer icons.

  6. 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.

Further Reading