BottomNavigation: What It Is, Structure, and Setup in Jetpack Compose

Author: IT Sectr Published: 2026-06-29 Reading time: 8 min

BottomNavigation (NavigationBar in Material3) is a Material Design component that provides navigation between three to five main screens of an application. In Jetpack Compose, the bottom bar is implemented via NavigationBar or BottomNavigation, where each item is represented by an icon and a text label. According to Google Material Design (2025), BottomNavigation is recommended for iOS and Android as the primary navigation pattern. The panel automatically adjusts to the system navigation bar height (Gesture Navigation) through WindowInsets. NavigationBar in Material3 supports dynamic color, badge, and adaptive insets.

Key Takeaways

  • BottomNavigation — a bottom navigation bar for 3–5 screens, implemented via NavigationBar in Material3
  • NavigationBarItem — a panel item containing an icon, label, and badge for notifications
  • Connection with NavHost — selecting an item triggers navigate() in NavController to switch the graph
  • Material3 adds Dynamic Color and automatic system navigation handling via WindowInsets
  • Badge — a notification badge on the icon, implemented via BadgedBox with a count

What Is BottomNavigation in Jetpack Compose?

BottomNavigation is a Material Design component for a bottom navigation bar that displays three to five menu items. In Jetpack Compose, it is implemented via the composable functions BottomNavigation (Material2) and NavigationBar (Material3). The bar is placed at the bottom of the screen and provides quick access to key sections of the application.

The main purpose of BottomNavigation is to organize primary navigation between screen-level destinations. Each panel item represents a separate screen (Home, Search, Profile). Tapping switches the content in the main area of the screen. BottomNavigation is not intended for nested navigation — for that, tabs at the top are used.

According to the Material Design Guidelines (2025), the bottom bar should only contain sections accessible from any point in the application. Hiding the bar on child screens (e.g., when opening a detail page) is a standard practice. In Compose, BottomNavigation visibility is controlled via Scaffold state or conditional rendering based on the current NavController route.

In Material3, it is recommended to use NavigationBar instead of the legacy BottomNavigation. NavigationBar includes Dynamic Color, WindowInsets support, and improved press animation. According to Google, NavigationBar is the forward-looking component, while BottomNavigation from M2 remains only for backward compatibility.

Each NavigationBar item is implemented via the NavigationBarItem composable. Parameters: icon (icon), label (text label), selected (selection state), and onClick (callback). NavigationBarItem automatically applies the selected color (primary) and animates the icon size change on activation.

kotlin
@Composable
fun BottomNavBar() {
    val items = listOf(
        Screen.Home,
        Screen.Search,
        Screen.Profile
    )
    var selectedItem by remember { mutableIntStateOf(0) }
    NavigationBar {
        items.forEachIndexed { index, item ->
            NavigationBarItem(
                icon = { Icon(item.icon, contentDescription = item.label) },
                label = { Text(item.label) },
                selected = selectedItem == index,
                onClick = { selectedItem = index }
            )
        }
    }
}

Icons for NavigationBarItem should be Filled for the selected state and Outlined for the unselected state. Material Icons provide both sets: Icons.Filled.Home and Icons.Outlined.Home. By default, NavigationBarItem displays the filled variant for selected. If different display is needed, pass the icon conditionally: if (selected) Filled.X else Outlined.X.

The text label length should be no more than 12 characters (Latin) or 8 characters (Cyrillic). Long labels are truncated with an ellipsis. For labels, use maxLines = 1 and ellipsis = TextTruncation.Ellipsis. If the label must always be shown, remove the alwaysShowLabel = true parameter (default is true).

ParameterTypeDescription
icon@Composable () -> UnitItem icon
label@Composable (() -> Unit)?Text label below the icon
selectedBooleanSelected item flag
onClick() -> UnitClick callback
badge@Composable (() -> Unit)?Badge with notification count

Connecting BottomNavigation with NavHost

Directly updating selectedItem does not trigger navigation — switching screens requires integration with NavController. The typical pattern: onClick in NavigationBarItem calls navController.navigate(route), and selectedItem is determined from the current NavController route.

NavController stores the current back stack. To determine which bar item is selected, compare the current route (navController.currentBackStackEntryAsState().value?.destination?.route) with the item routes. If the route matches, the item is considered selected. This eliminates state duplication.

kotlin
@Composable
fun MainScreen(navController: NavController) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    Scaffold(
        bottomBar = {
            NavigationBar {
                items.forEach { item ->
                    NavigationBarItem(
                        icon = { Icon(item.icon, contentDescription = item.label) },
                        label = { Text(item.label) },
                        selected = currentRoute == item.route,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) { /* content */ }
}

Key settings in navigate(): popUpTo(startDestination) prevents back stack accumulation when switching tabs, launchSingleTop prevents route duplication in the stack, and restoreState restores the state of the previously visited tab. Without these parameters, each click creates a new entry in the stack, leading to incorrect Back button behavior.

NavigationBar in Material3: Dynamic Color and Adaptation

Material3 (M3) provides NavigationBar and NavigationBarItem as a replacement for BottomNavigation from Material2. Key advantages of M3: Dynamic Color, adaptive height, full-width ripple animation, and WindowInsets support for system navigation handling.

Dynamic Color automatically picks NavigationBar colors based on the device wallpaper (Android 12+). The active item is colored in primary, inactive ones in onSurfaceVariant. By default, NavigationBar rises above the system navigation bar via Modifier.navigationBarsPadding(). For manual control, use the windowInsets parameter.

NavigationBar in M3 automatically animates the active item width — the icon shifts left and the label expands. The animation lasts 300 ms and uses standard Compose easing. If this behavior is not desired, always show the label via alwaysShowLabel = true in NavigationBarItem.

Customizing NavigationBar Colors

To override colors, pass NavigationBarColors in the colors parameter. By default, NavigationBarDefaults.colors() is used, but custom values can be set: containerColor (bar background), contentColor (icon colors), indicatorColor (active item background). Unlike M2 where colors were set via BottomNavigationDefaults, M3 uses a unified NavColors interface.

If the project uses BottomNavigation from M2 (androidx.compose.material), Google recommends migrating to M3 NavigationBar. Migration includes replacing imports, updating parameters, and adding @OptIn(ExperimentalMaterial3Api::class). BottomNavigation from M2 is considered deprecated and does not receive new features.

The NavigationBar state stores the index of the currently selected item. In simple cases, remember { mutableIntStateOf(0) } is used. However, for production applications, the index should be synchronized with the navigation graph, not be a local UI state.

The main approaches to state management: NavController-based (recommended) and ViewModel-based. In the first case, selectedItem is computed from the current NavController route. In the second, the ViewModel stores the current screen, and NavigationBarItem reads the StateFlow from the ViewModel. NavController-based is simpler, ViewModel-based provides more control for complex logic (authentication, A/B tests).

When using saveState and restoreState, NavController automatically saves and restores the state of LazyColumn and other components inside each tab. This eliminates the need for a ViewModel to store scroll position. However, saving does not apply to input fields — they require SavedStateHandle or ViewModel.

Notification Badges and Counters on Items

Badge is a compact notification indicator displayed on the NavigationBarItem icon. In Material3, the badge is implemented via BadgedBox, which wraps the icon and places the badge in the top-right corner. The badge can contain a number (notification count) or a dot (simple presence indicator).

kotlin
@Composable
fun HomeItem(unreadCount: Int) {
    NavigationBarItem(
        icon = {
            BadgedBox(badge = {
                if (unreadCount > 0) {
                    Badge { Text(unreadCount.toString()) }
                }
            }) {
                Icon(Icons.Filled.Home, contentDescription = "Home")
            }
        },
        label = { Text("Home") },
        selected = false,
        onClick = { }
    )
}

BadgedBox automatically positions the Badge in the top-right corner of the child element. If the count exceeds 99, the Badge displays "99+". To hide the badge at zero, use if (count > 0). The Material3 Badge defaults to error color (red) with white text. Customize via BadgeDefaults.colors(containerColor, contentColor).

In Material2, the badge was absent — it had to be drawn manually via Canvas or using third-party libraries. Material3 solved this with a native component. According to Android Developers (2025), BadgedBox is used in 40% of apps with NavigationBar, 60% of which display unread messages.

Frequently Asked Questions

How many items should be in BottomNavigation?

Material Design recommends 3 to 5 items. Fewer than three is impractical (use Tabs instead). More than five reduces label readability and touch target size. If more than 5 sections are needed, use Navigation Rail (tablets) or Drawer.

How to hide BottomNavigation on child screens?

Define a list of routes where the bar is visible (usually root routes). In Scaffold bottomBar, pass NavigationBar only if currentRoute in bottomNavRoutes. An alternative is NavHost with a separate graph for nested screens without bottomBar.

What is the difference between NavigationBar (M3) and BottomNavigation (M2)?

NavigationBar from Material3 supports Dynamic Color, BadgedBox, WindowInsets, and animated active item width. BottomNavigation from Material2 is a legacy API without these features. Google recommends using NavigationBar for new projects and migrating existing ones.

Why does the screen recreate when clicking a BottomNavigation item?

The issue is the absence of saveState and restoreState in navigate(). Without them, NavController does not save the previous tab's state. Add popUpTo(startDestination) { saveState = true } and restoreState = true in NavOptions to preserve LazyColumn, scroll position, and input fields.

How to customize the switching animation between tabs?

The switching animation is controlled at the NavHost level, not BottomNavigation. Add composable(route, enterTransition, exitTransition) with custom animations. For BottomNavigation, fading animation (fadeIn + fadeOut) with 300ms duration is typical.

Summary

  • BottomNavigation (NavigationBar in M3) — a bottom bar for switching between 3–5 main screens of the application
  • NavigationBarItem — a panel item with an icon, label, badge, and selection state
  • Integration with NavController requires popUpTo, launchSingleTop, and restoreState for correct stack management
  • Material3 NavigationBar supports Dynamic Color, BadgedBox, WindowInsets, and active item animation
  • BadgedBox — a component for notification badges with a number or dot on the item icon
  • Selection state is computed from currentBackStackEntryAsState for synchronization with NavController
  • To hide the bar on child screens, use conditional rendering in Scaffold bottomBar based on a route list

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also