skip to content

In Flutter's Material 3, how do NavigationBar and NavigationRail manage the selected destination, and when do you use each instead of BottomNavigationBar?

level: middleimportance: should knowfreq 42%

answer

  1. the parent owns the index
  2. selectedIndex plus onDestinationSelected
  3. at least two destinations
  4. bar at the bottom, rail at the side
  5. extended rail needs no labelType

basics

~10 s

Neither widget stores the selection: you pass selectedIndex and update it in onDestinationSelected. NavigationBar is the Material 3 bottom bar that supersedes BottomNavigationBar on phones; NavigationRail is the vertical version for wider screens.

solid answer

~50 s

`NavigationBar` and `NavigationRail` are controlled widgets: they draw whatever `selectedIndex` they are given and call `onDestinationSelected(int)` on a tap, so the parent stores the index and rebuilds. `NavigationBar` goes in `Scaffold.bottomNavigationBar`, takes a list of `NavigationDestination` (an `icon`, optional `selectedIcon` and a `String` label), needs at least two, defaults `selectedIndex` to `0`, and in Material 3 is 80 pixels tall with labels always shown. It is the Material 3 counterpart of `BottomNavigationBar`, which is an older Material 2 component rather than a deprecated one. `NavigationRail` is vertical, placed in a `Row` beside the body on tablets and desktop, takes `NavigationRailDestination` with a widget label, allows a `null` `selectedIndex`, and shows labels by `labelType` or, with `extended: true`, beside the icons. For a bike-rental app I show the bar under 600 pixels of width and the rail above.

code

dart · 45 lines
dart
class _RentalShellState extends State<RentalShell> {
  int _index = 0;

  static const _pages = [MapPage(), RidesPage(), AccountPage()];

  void _select(int index) => setState(() => _index = index);

  @override
  Widget build(BuildContext context) {
    final wide = MediaQuery.sizeOf(context).width >= 600;
    final body = IndexedStack(index: _index, children: _pages);
    if (wide) {
      return Scaffold(
        body: Row(
          children: [
            NavigationRail(
              selectedIndex: _index,
              onDestinationSelected: _select,
              labelType: NavigationRailLabelType.all,
              destinations: const [
                NavigationRailDestination(icon: Icon(Icons.map_outlined), label: Text('Map')),
                NavigationRailDestination(icon: Icon(Icons.pedal_bike), label: Text('Rides')),
                NavigationRailDestination(icon: Icon(Icons.person_outline), label: Text('Account')),
              ],
            ),
            const VerticalDivider(width: 1),
            Expanded(child: body),
          ],
        ),
      );
    }
    return Scaffold(
      body: body,
      bottomNavigationBar: NavigationBar(
        selectedIndex: _index,
        onDestinationSelected: _select,
        destinations: const [
          NavigationDestination(icon: Icon(Icons.map_outlined), selectedIcon: Icon(Icons.map), label: 'Map'),
          NavigationDestination(icon: Icon(Icons.pedal_bike), label: 'Rides'),
          NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Account'),
        ],
      ),
    );
  }
}

go deeper

for a junior

Recall that NavigationBar is the Material 3 bottom bar, that you pass selectedIndex and update it in onDestinationSelected, and that NavigationRail is the side version.

for a middle

Explain the controlled-widget pattern, the destination types and their label types, the two-destination minimum, and the extended rail's labelType rule.

for a senior

Show how one index drives both bar and rail across widths, and how page state is kept or dropped when switching sections.

for a principal

Decide who owns the selected section, widget state or the router, so that deep links, back behaviour and layout changes all agree.

## Controlled widgets Both widgets are **controlled**: they do not remember which destination is selected. The app passes `selectedIndex`, and when the user taps, the widget calls `onDestinationSelected` with the tapped index. If the handler does not store the new index and rebuild, the highlight never moves. This is deliberate: the selection usually drives more than the bar (which page the body shows, the URL, analytics), so it belongs in the app's state, not in the widget. ## NavigationBar `NavigationBar` is the Material 3 bottom navigation component. Key facts in Flutter 3.47: - `destinations` is a list of widgets, usually `NavigationDestination(icon:, selectedIcon:, label:)`, where `label` is a `String`; - there must be at least two destinations, checked by an assertion; - `selectedIndex` defaults to `0` and must be within range; - the Material 3 defaults are a height of 80 and `NavigationDestinationLabelBehavior.alwaysShow`, with a pill-shaped indicator behind the selected icon; - it goes in `Scaffold.bottomNavigationBar`. ## NavigationRail `NavigationRail` is the vertical form for wide layouts. It is not a Scaffold slot; it sits in a `Row` next to the body, usually followed by a `VerticalDivider`. - `destinations` are `NavigationRailDestination(icon:, selectedIcon:, label:)`, where `label` is a widget; - `selectedIndex` is required but nullable, so a rail can show no selection; - `labelType` is one of `NavigationRailLabelType.none`, `.selected` or `.all`; - `extended: true` shows labels beside the icons, and then `labelType` must be `null` or `none`, or an assertion fails; - `leading` and `trailing` hold extras such as a floating action button. ## Choosing the component | Component | Where | When | |---|---|---| | `NavigationBar` | bottom of the screen | phones, three to five top-level destinations | | `NavigationRail` | side of the screen | tablets, foldables, desktop | | `NavigationDrawer` | side sheet | many destinations or secondary ones | | `BottomNavigationBar` | bottom | Material 2 apps; it is not updated to Material 3 styling | `BottomNavigationBar` still exists and is not deprecated, but the framework's Material 3 component list names `NavigationBar` as its replacement. A Material 3 app mixing the two looks inconsistent. ## Switching bodies The index usually picks the body. Two common patterns: 1. Build only the selected page, `pages[_index]`: simple, but each switch rebuilds the page and loses its scroll position and local state. 2. Keep all pages alive in an `IndexedStack` and show one: state survives, at the cost of building every page up front. Apps with deep links usually let the router own the index instead, with one navigation stack per tab; that is a routing concern. ## Bike-rental example The rental app has three destinations: Map, Rides and Account. On a phone, a `NavigationBar` sits in the Scaffold's bottom slot. On a tablet wider than about 600 logical pixels, the same destinations appear in a `NavigationRail` beside the map. Both read the same `_index` from the screen's state, so rotating the device keeps the current section selected. ## Common mistakes - **Keeping the index in a local variable inside `build`.** It is reset to its initial value on every rebuild; the index must live in a `State` field or a state object. - **Too many destinations.** Bottom and rail navigation are meant for a small set of top-level sections, typically three to five; more belong in a `NavigationDrawer` or a screen of their own. - **Mixing `BottomNavigationBar` into a Material 3 app.** It works, but it keeps Material 2 styling next to Material 3 components. - **Putting `NavigationRail` in a Scaffold slot.** It has no slot; it goes in a `Row` beside the body, usually with a `VerticalDivider`. - **Rebuilding pages on every switch without meaning to.** Choose deliberately between building only the selected page and keeping all pages alive.

  • What goes wrong if the selected page is built as pages[_index] instead of an IndexedStack?
    Switching tabs removes the old page from the tree, so its `State` is disposed: scroll position, text fields and any loaded data held locally are lost, and coming back rebuilds it from scratch. An `IndexedStack` keeps every page mounted and only paints the selected one, which preserves state but builds all pages up front.
  • Why does NavigationRail assert when extended is true and labelType is all?
    An extended rail already shows each label beside its icon. `labelType` controls labels drawn under the icons in the compact rail, so combining the two would show labels twice. The constructor therefore requires `labelType` to be `null` or `NavigationRailLabelType.none` whenever `extended` is true.

saying these in an interview costs you the question

  • NavigationBar tracks the selected tab internally once you set an initial index.
  • BottomNavigationBar was removed in Material 3.
  • A NavigationBar with a single destination is fine for a one-section app.
  • NavigationRail goes in the Scaffold's drawer slot.
  • NavigationRail labels are plain strings, like NavigationBar's.