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 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.
@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).
| Parameter | Type | Description |
|---|---|---|
| icon | @Composable () -> Unit | Item icon |
| label | @Composable (() -> Unit)? | Text label below the icon |
| selected | Boolean | Selected item flag |
| onClick | () -> Unit | Click callback |
| badge | @Composable (() -> Unit)? | Badge with notification count |
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.
@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.
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.
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.
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).
@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
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.
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.
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.
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.
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
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.
Read also