BottomAppBar and NavigationRail in Material 3

Quick answer: use BottomAppBar for important actions on the current screen, often alongside a floating action button. Use NavigationRail for persistent switching between top-level destinations when a window has enough horizontal room. If compact screens need three to five top-level destinations, use Material 3 NavigationBar instead of repurposing a bottom app bar.

Both composables live near the edge of the screen, but they solve different problems. Mixing actions such as “edit” and “share” with destinations such as “home” and “settings” makes the navigation model harder to learn and test.

Choose the component by the user’s job

NeedUseWhy
A few key actions for the visible screenBottomAppBarIt keeps screen-level controls and an optional FAB at the bottom edge.
Three to five equal top-level destinations on a compact windowNavigationBarIt is Material 3’s bottom destination-navigation component.
Persistent top-level destinations on a wider windowNavigationRailThe vertical rail preserves content height and supports a wider workspace.
One navigation model that changes with available spaceNavigationSuiteScaffoldThe adaptive suite can select the appropriate navigation component.

The Material 3 migration guide is explicit about the terminology: Material 2 BottomNavigation became Material 3 NavigationBar. That is separate from BottomAppBar, which is an app bar for actions and an optional FAB.

Add a BottomAppBar through Scaffold

Scaffold is the natural screen shell for a bottom app bar. Give the bar actions that apply to the current screen; do not make a destructive action an unlabeled icon just to fill the row.

import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun NoteScreen(
    onEdit: () -> Unit,
    onShare: () -> Unit,
    onCreate: () -> Unit,
) {
    Scaffold(
        bottomBar = {
            BottomAppBar(
                actions = {
                    IconButton(onClick = onEdit) {
                        Icon(Icons.Default.Edit, contentDescription = "Edit note")
                    }
                    IconButton(onClick = onShare) {
                        Icon(Icons.Default.Share, contentDescription = "Share note")
                    }
                },
                floatingActionButton = {
                    FloatingActionButton(
                        onClick = onCreate,
                        containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
                        elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
                    ) {
                        Icon(Icons.Default.Add, contentDescription = "Create note")
                    }
                },
            )
        },
    ) { innerPadding ->
        Text(
            text = "Current note",
            modifier = Modifier.padding(innerPadding),
        )
    }
}

The official app-bar guide documents the actions and floatingActionButton slots. A BottomAppBar can also receive custom content, but start with its semantic slots when the design follows the standard action-bar pattern.

Respect the Scaffold content padding

Scaffold passes innerPadding to its content. Apply it so content is not hidden behind the app bar or system UI:

Scaffold(bottomBar = { /* BottomAppBar */ }) { innerPadding ->
    LazyColumn(
        contentPadding = innerPadding,
        modifier = Modifier.consumeWindowInsets(innerPadding),
    ) {
        // Items
    }
}

For a non-scrolling layout, Modifier.padding(innerPadding) is normally enough. For a scrollable container, use the padding as content padding and consume it as shown. Do not add independent system-bar padding on top without checking the result: built-in Material 3 app bars handle their own expected insets, while Scaffold gives its content the padding values to consume. The Material 3 inset guidance explains this division of responsibility.

Build a NavigationRail for top-level destinations

A rail is a navigation component, so its state should be owned above the rail. The rail renders the selected destination and forwards an event; it should not create the app’s navigation controller or decide which screen to show.

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

enum class AppDestination(val label: String) {
    Home("Home"),
    Profile("Profile"),
    Settings("Settings"),
}

@Composable
fun WideAppShell(
    selectedDestination: AppDestination,
    onDestinationSelected: (AppDestination) -> Unit,
) {
    Row(Modifier.fillMaxSize()) {
        NavigationRail {
            NavigationRailItem(
                selected = selectedDestination == AppDestination.Home,
                onClick = { onDestinationSelected(AppDestination.Home) },
                icon = { Icon(Icons.Default.Home, contentDescription = null) },
                label = { Text(AppDestination.Home.label) },
            )
            NavigationRailItem(
                selected = selectedDestination == AppDestination.Profile,
                onClick = { onDestinationSelected(AppDestination.Profile) },
                icon = { Icon(Icons.Default.Person, contentDescription = null) },
                label = { Text(AppDestination.Profile.label) },
            )
            NavigationRailItem(
                selected = selectedDestination == AppDestination.Settings,
                onClick = { onDestinationSelected(AppDestination.Settings) },
                icon = { Icon(Icons.Default.Settings, contentDescription = null) },
                label = { Text(AppDestination.Settings.label) },
            )
        }

        // Destination content goes here.
    }
}

The selected value must reflect the actual back-stack destination, not only the last icon clicked. In a Navigation Compose app, derive it from the current destination and navigate from onDestinationSelected. Navigation Compose: Create Your First NavHost covers the route and NavHost side of that connection.

The label is visible text for a navigation destination. The example leaves each icon’s content description null because the item label supplies the accessible name; do not omit that label or a meaningful icon description in an icon-only variation.

When a rail should replace a bottom navigation bar

Do not select a rail by device name alone. It belongs on a layout where its persistent vertical footprint is useful and enough width remains for destination content. A tablet in split-screen may be narrow; a phone in landscape may have different constraints than a portrait phone.

Keep the destination list and navigation events independent of the UI container. Then the same destinations can be rendered in a compact NavigationBar or a wider NavigationRail without duplicating route definitions, selected-state logic, analytics, or accessibility labels.

For new adaptive apps, consider the Material 3 adaptive navigation suite. NavigationSuiteScaffold comes from androidx.compose.material3:material3-adaptive-navigation-suite and selects an appropriate navigation type from adaptive window information. The adaptive-navigation guide has the current dependency and setup details. Its API evolves independently from the basic rail and bar components, so follow the version and opt-in guidance for the dependency you adopt.

Common mistakes

Using BottomAppBar as a destination bar

An app bar can technically hold navigation icons, but NavigationBarItem and NavigationRailItem provide the destination semantics and selected state that top-level navigation needs. Use a bottom app bar for actions, not as a look-alike destination selector.

Keeping both a rail and a navigation bar visible

Show one primary destination-navigation pattern for a window size. Duplicating the same destinations in a rail and a bottom bar wastes space and creates two places that can disagree about selection.

Ignoring inset and content padding

If a list is placed behind a bottom bar, users can lose its final row or action. Apply Scaffold’s provided padding once and verify edge-to-edge behavior on a real device.

Letting the rail own selection state

State belongs in the screen or navigation layer. Keep rails and bars small, stateless renderers that receive selected and emit clicks. This matches the state ownership approach in State Hoisting with Real Examples.

Test both navigation and actions

  • Verify that each selected rail item matches the visible NavHost destination.
  • Test back navigation and state restoration after rotation or process recreation.
  • Check the bottom app bar with gesture navigation and a three-button navigation device.
  • Confirm every action and destination has an accessible label and a sufficiently large touch target.
  • Review compact, medium, expanded, and split-screen windows before choosing a rail or an adaptive suite.

A practical rule

Use BottomAppBar to keep current-screen actions available. Use NavigationRail to make top-level destinations persistent on wide layouts. Keep both pieces connected to hoisted navigation state, honor Scaffold padding, and move to the adaptive navigation suite when one app must choose the right navigation container across window sizes.