← Back to Blog

Flutter NavigationRail Localization: Vertical Navigation for Multilingual Apps

flutternavigationrailnavigationmateriallocalizationrtl

Flutter NavigationRail Localization: Vertical Navigation for Multilingual Apps

NavigationRail is a Flutter widget that provides a vertical navigation bar typically used alongside content in wide-screen layouts. In multilingual applications, NavigationRail is essential for displaying translated destination labels in a vertical rail, adapting label positioning for different translation lengths, supporting RTL layouts where the rail may appear on the opposite side, and providing accessible navigation announcements in the active language.

Understanding NavigationRail in Localization Context

NavigationRail renders a vertical strip of navigation destinations with icons and optional labels. For multilingual apps, this enables:

  • Translated destination labels displayed below or beside icons
  • Adaptive label layout that handles long translations gracefully
  • RTL-aware rail placement on the correct side of the screen
  • Localized tooltip text for compact rail mode

Why NavigationRail Matters for Multilingual Apps

NavigationRail provides:

  • Vertical layout: Translated labels stacked vertically avoid the width constraints of bottom navigation
  • Label visibility control: Show labels always, only when selected, or never -- useful when translations vary in length
  • Leading/trailing widgets: Space for localized headers, user info, or action buttons above and below destinations
  • Extended mode: An expanded state that shows full translated labels alongside icons

Basic NavigationRail Implementation

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

class LocalizedNavigationRailExample extends StatefulWidget {
  const LocalizedNavigationRailExample({super.key});

  @override
  State<LocalizedNavigationRailExample> createState() =>
      _LocalizedNavigationRailExampleState();
}

class _LocalizedNavigationRailExampleState
    extends State<LocalizedNavigationRailExample> {
  int _selectedIndex = 0;

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

    return Scaffold(
      body: Row(
        children: [
          NavigationRail(
            selectedIndex: _selectedIndex,
            onDestinationSelected: (index) {
              setState(() => _selectedIndex = index);
            },
            labelType: NavigationRailLabelType.all,
            destinations: [
              NavigationRailDestination(
                icon: const Icon(Icons.home_outlined),
                selectedIcon: const Icon(Icons.home),
                label: Text(l10n.homeLabel),
              ),
              NavigationRailDestination(
                icon: const Icon(Icons.search_outlined),
                selectedIcon: const Icon(Icons.search),
                label: Text(l10n.searchLabel),
              ),
              NavigationRailDestination(
                icon: const Icon(Icons.favorite_outline),
                selectedIcon: const Icon(Icons.favorite),
                label: Text(l10n.favoritesLabel),
              ),
              NavigationRailDestination(
                icon: const Icon(Icons.person_outline),
                selectedIcon: const Icon(Icons.person),
                label: Text(l10n.profileLabel),
              ),
            ],
          ),
          const VerticalDivider(thickness: 1, width: 1),
          Expanded(
            child: _buildContent(l10n),
          ),
        ],
      ),
    );
  }

  Widget _buildContent(AppLocalizations l10n) {
    final titles = [
      l10n.homeLabel,
      l10n.searchLabel,
      l10n.favoritesLabel,
      l10n.profileLabel,
    ];

    return Center(
      child: Text(
        titles[_selectedIndex],
        style: Theme.of(context).textTheme.headlineMedium,
      ),
    );
  }
}

Advanced NavigationRail Patterns for Localization

Extended Rail with Localized Labels

An extended NavigationRail that shows full translated labels alongside icons for wide screens.

class ExtendedNavigationRailExample extends StatefulWidget {
  const ExtendedNavigationRailExample({super.key});

  @override
  State<ExtendedNavigationRailExample> createState() =>
      _ExtendedNavigationRailExampleState();
}

class _ExtendedNavigationRailExampleState
    extends State<ExtendedNavigationRailExample> {
  int _selectedIndex = 0;
  bool _isExtended = true;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final screenWidth = MediaQuery.sizeOf(context).width;
    final showExtended = screenWidth > 800 && _isExtended;

    return Scaffold(
      body: Row(
        children: [
          NavigationRail(
            extended: showExtended,
            selectedIndex: _selectedIndex,
            onDestinationSelected: (index) {
              setState(() => _selectedIndex = index);
            },
            leading: showExtended
                ? Padding(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 16,
                      vertical: 8,
                    ),
                    child: Text(
                      l10n.appName,
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                  )
                : FloatingActionButton.small(
                    onPressed: () {},
                    tooltip: l10n.composeTooltip,
                    child: const Icon(Icons.add),
                  ),
            trailing: Expanded(
              child: Align(
                alignment: Alignment.bottomCenter,
                child: Padding(
                  padding: const EdgeInsets.only(bottom: 16),
                  child: IconButton(
                    icon: Icon(
                      showExtended
                          ? Icons.chevron_left
                          : Icons.chevron_right,
                    ),
                    tooltip: showExtended
                        ? l10n.collapseRailTooltip
                        : l10n.expandRailTooltip,
                    onPressed: () {
                      setState(() => _isExtended = !_isExtended);
                    },
                  ),
                ),
              ),
            ),
            destinations: [
              NavigationRailDestination(
                icon: const Icon(Icons.inbox_outlined),
                selectedIcon: const Icon(Icons.inbox),
                label: Text(l10n.inboxLabel),
              ),
              NavigationRailDestination(
                icon: const Icon(Icons.send_outlined),
                selectedIcon: const Icon(Icons.send),
                label: Text(l10n.sentLabel),
              ),
              NavigationRailDestination(
                icon: const Icon(Icons.drafts_outlined),
                selectedIcon: const Icon(Icons.drafts),
                label: Text(l10n.draftsLabel),
              ),
              NavigationRailDestination(
                icon: const Icon(Icons.delete_outline),
                selectedIcon: const Icon(Icons.delete),
                label: Text(l10n.trashLabel),
              ),
            ],
          ),
          const VerticalDivider(thickness: 1, width: 1),
          Expanded(
            child: Center(
              child: Text(l10n.selectMessagePrompt),
            ),
          ),
        ],
      ),
    );
  }
}

Adaptive Layout with Badge Counts

NavigationRail with localized badge counts that adapts between rail and bottom navigation based on screen width.

class AdaptiveNavigationExample extends StatefulWidget {
  const AdaptiveNavigationExample({super.key});

  @override
  State<AdaptiveNavigationExample> createState() =>
      _AdaptiveNavigationExampleState();
}

class _AdaptiveNavigationExampleState
    extends State<AdaptiveNavigationExample> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final screenWidth = MediaQuery.sizeOf(context).width;
    final useRail = screenWidth >= 600;

    final destinations = [
      _NavItem(
        icon: Icons.dashboard_outlined,
        selectedIcon: Icons.dashboard,
        label: l10n.dashboardLabel,
        badgeCount: 0,
      ),
      _NavItem(
        icon: Icons.notifications_outlined,
        selectedIcon: Icons.notifications,
        label: l10n.notificationsLabel,
        badgeCount: 5,
      ),
      _NavItem(
        icon: Icons.message_outlined,
        selectedIcon: Icons.message,
        label: l10n.messagesLabel,
        badgeCount: 12,
      ),
      _NavItem(
        icon: Icons.settings_outlined,
        selectedIcon: Icons.settings,
        label: l10n.settingsLabel,
        badgeCount: 0,
      ),
    ];

    if (useRail) {
      return Scaffold(
        body: Row(
          children: [
            NavigationRail(
              selectedIndex: _selectedIndex,
              onDestinationSelected: (index) {
                setState(() => _selectedIndex = index);
              },
              labelType: NavigationRailLabelType.selected,
              destinations: destinations.map((dest) {
                return NavigationRailDestination(
                  icon: dest.badgeCount > 0
                      ? Badge(
                          label: Text('${dest.badgeCount}'),
                          child: Icon(dest.icon),
                        )
                      : Icon(dest.icon),
                  selectedIcon: dest.badgeCount > 0
                      ? Badge(
                          label: Text('${dest.badgeCount}'),
                          child: Icon(dest.selectedIcon),
                        )
                      : Icon(dest.selectedIcon),
                  label: Text(dest.label),
                );
              }).toList(),
            ),
            const VerticalDivider(thickness: 1, width: 1),
            Expanded(
              child: Center(
                child: Text(destinations[_selectedIndex].label),
              ),
            ),
          ],
        ),
      );
    }

    return Scaffold(
      body: Center(
        child: Text(destinations[_selectedIndex].label),
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() => _selectedIndex = index);
        },
        destinations: destinations.map((dest) {
          return NavigationDestination(
            icon: dest.badgeCount > 0
                ? Badge(
                    label: Text('${dest.badgeCount}'),
                    child: Icon(dest.icon),
                  )
                : Icon(dest.icon),
            selectedIcon: dest.badgeCount > 0
                ? Badge(
                    label: Text('${dest.badgeCount}'),
                    child: Icon(dest.selectedIcon),
                  )
                : Icon(dest.selectedIcon),
            label: dest.label,
          );
        }).toList(),
      ),
    );
  }
}

class _NavItem {
  final IconData icon;
  final IconData selectedIcon;
  final String label;
  final int badgeCount;

  _NavItem({
    required this.icon,
    required this.selectedIcon,
    required this.label,
    required this.badgeCount,
  });
}

Grouped Destinations with Section Headers

NavigationRail with localized section headers separating groups of destinations.

class GroupedNavigationRail extends StatefulWidget {
  const GroupedNavigationRail({super.key});

  @override
  State<GroupedNavigationRail> createState() => _GroupedNavigationRailState();
}

class _GroupedNavigationRailState extends State<GroupedNavigationRail> {
  int _selectedIndex = 0;

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

    return Scaffold(
      body: Row(
        children: [
          SizedBox(
            width: 200,
            child: Column(
              children: [
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Text(
                    l10n.appName,
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                ),
                const Divider(),
                _SectionHeader(title: l10n.mainSectionLabel),
                _buildDestination(0, Icons.home, l10n.homeLabel),
                _buildDestination(1, Icons.explore, l10n.exploreLabel),
                _buildDestination(2, Icons.bookmark, l10n.savedLabel),
                const Divider(),
                _SectionHeader(title: l10n.manageSectionLabel),
                _buildDestination(3, Icons.analytics, l10n.analyticsLabel),
                _buildDestination(4, Icons.people, l10n.teamLabel),
                const Divider(),
                _SectionHeader(title: l10n.accountSectionLabel),
                _buildDestination(5, Icons.settings, l10n.settingsLabel),
                _buildDestination(6, Icons.help, l10n.helpLabel),
              ],
            ),
          ),
          const VerticalDivider(thickness: 1, width: 1),
          const Expanded(
            child: Center(child: Placeholder()),
          ),
        ],
      ),
    );
  }

  Widget _buildDestination(int index, IconData icon, String label) {
    final isSelected = _selectedIndex == index;
    return ListTile(
      leading: Icon(icon),
      title: Text(label),
      selected: isSelected,
      onTap: () => setState(() => _selectedIndex = index),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  final String title;
  const _SectionHeader({required this.title});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsetsDirectional.fromSTEB(16, 8, 16, 4),
      child: Text(
        title.toUpperCase(),
        style: Theme.of(context).textTheme.labelSmall?.copyWith(
              color: Theme.of(context).colorScheme.onSurfaceVariant,
              letterSpacing: 1.2,
            ),
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

In RTL layouts, NavigationRail should typically appear on the right side of the screen. Use Directionality to ensure proper placement.

class BidirectionalNavigationRail extends StatefulWidget {
  const BidirectionalNavigationRail({super.key});

  @override
  State<BidirectionalNavigationRail> createState() =>
      _BidirectionalNavigationRailState();
}

class _BidirectionalNavigationRailState
    extends State<BidirectionalNavigationRail> {
  int _selectedIndex = 0;

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

    final rail = NavigationRail(
      selectedIndex: _selectedIndex,
      onDestinationSelected: (index) {
        setState(() => _selectedIndex = index);
      },
      labelType: NavigationRailLabelType.all,
      destinations: [
        NavigationRailDestination(
          icon: const Icon(Icons.home_outlined),
          selectedIcon: const Icon(Icons.home),
          label: Text(l10n.homeLabel),
        ),
        NavigationRailDestination(
          icon: const Icon(Icons.category_outlined),
          selectedIcon: const Icon(Icons.category),
          label: Text(l10n.categoriesLabel),
        ),
        NavigationRailDestination(
          icon: const Icon(Icons.shopping_cart_outlined),
          selectedIcon: const Icon(Icons.shopping_cart),
          label: Text(l10n.cartLabel),
        ),
      ],
    );

    final content = Expanded(
      child: Center(
        child: Text(l10n.contentPlaceholder),
      ),
    );

    return Scaffold(
      body: Row(
        textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
        children: [
          rail,
          const VerticalDivider(thickness: 1, width: 1),
          content,
        ],
      ),
    );
  }
}

Testing NavigationRail 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 LocalizedNavigationRailExample(),
    );
  }

  testWidgets('NavigationRail renders localized labels', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(NavigationRail), findsOneWidget);
  });

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

  testWidgets('Destination selection updates state', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    final icons = find.byType(NavigationRailDestination);
    expect(icons, findsNWidgets(4));
  });
}

Best Practices

  1. Use NavigationRailLabelType.selected for compact rails where only the active destination shows its translated label, reducing visual clutter with long translations.

  2. Switch between NavigationRail and NavigationBar based on screen width using MediaQuery to provide the best navigation experience on both wide and narrow screens.

  3. Provide leading and trailing widgets with translated tooltips for actions like compose buttons or collapse/expand controls above and below destinations.

  4. Use extended mode on wide screens to show full translated labels alongside icons, improving discoverability for users unfamiliar with icon meanings.

  5. Place the rail on the correct side for RTL by controlling the Row direction or using Directionality to ensure the rail appears on the right side for RTL languages.

  6. Test destination labels with verbose translations to ensure they don't overflow or clip in both compact and extended rail modes.

Conclusion

NavigationRail provides a vertical navigation pattern for wide-screen Flutter layouts. For multilingual apps, it handles translated destination labels with configurable visibility, supports extended mode for full translated labels, and adapts placement for RTL layouts. By combining NavigationRail with adaptive layout switching, badge counts, and grouped section headers, you can build navigation experiences that work naturally in every supported language and screen size.

Further Reading