In today’s tutorial, we will learn how to implement a bottom tab navigation bar with a Tab Indicator in Kotlin Compose Multiplatform for both the Android and iOS platforms. The important thing is that we are not using any navigation library in our tutorial. We are simply using the BottomNavigation component, and it’s doing all the magic.
Bottom Tab Navigation in Compose Multiplatform KMP KMM
A Bottom tab navigation bar is a UI component in mobile applications that shows a Bottom navigation bar with Icons and Tab names. It allows the user to navigate between screens by simply tapping on the Icon. With Kotlin Compose Multiplatform, we can create a Custom Bottom tab with a single codebase for both Android and iOS platforms.
Icons:
I am using these icons. If you like, you can use these icons.
1. Configure Bottom Tab Icons:
1. Please download the icons from above, or you can use your own icons, and then paste the icons into your
APP -> composeApp -> src -> commonMain -> composeResource -> drawable folder.
2. After putting the icons in the composeResource folder, the icons will become a common resource for both the Android and iOS platforms.
3. To make the resource available for your project, you have to clean and rebuild the project.
2. Creating Required Bottom Tab Screens:
1. Create a folder named screens in your app folder. We will be creating 5 screens inside this folder.
2. Creating HomeScreen.kt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun HomeScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth() .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Home Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
3. Creating ListScreen.kt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun ListScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth(). fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "List Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
4. Creating MenuScreen.kt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun MenuScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth() .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Menu Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
5. Creating ProfileScreen.kt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun ProfileScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth() .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Profile Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
6. Creating SettingsScreen.kt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun SettingsScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth() .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Settings Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
3. Start Coding for App:
1. Open your project’s main App.kt file and start coding for bottom tabs integration.
2. Creating a State named selectedTab. It is used to hold the currently selected bottom tab index.
1 |
var selectedTab by remember { mutableStateOf(0) } |
3. Creating 3 modifications to set the Icon size and active and inactive bottom tab colors.
1 2 3 |
val iconModifier = Modifier.size(width = 28.dp, height = 24.dp) val activeBottomTabIconColor = Color(0xFFEEBA00) val inActiveBottomTabIconColor = Color(0XFF283d64) |
4. Creating 5 variables and storing the bottom tab names. The tab names are home, list, menu, profile and Settings.
1 2 3 4 5 |
val home = "Home" val list = "List" val menu = "Menu" val profile = "Profile" val settings = "Settings" |
5. Setting up the bottom tabs’ names as tabs to be used as labels in the bottom tabs.
1 2 3 4 5 |
val tabs = remember() { listOf( home, list, menu, profile, settings ) } |
6. Creating the BottomNavigation component in the Scaffold component.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
Scaffold(bottomBar = { Column { BottomNavigation( modifier = Modifier.height(70.dp), backgroundColor = Color.White ) { tabs.forEachIndexed { index, tab -> val isActive = selectedTab == index val currentIcon = when (index) { 0 -> painterResource(Res.drawable.home_icon) 1 -> painterResource(Res.drawable.list_icon) 2 -> painterResource(Res.drawable.menu_icon) 3 -> painterResource(Res.drawable.profile_icon) 4 -> painterResource(Res.drawable.settings_icon) else -> throw IllegalArgumentException("Unknown tab: $tab") } BottomNavigationItem( icon = { Column( modifier = Modifier .wrapContentSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (isActive) { Divider( modifier = Modifier .fillMaxWidth() .height(1.dp), color = activeBottomTabIconColor ) Spacer(modifier = Modifier.height(12.dp)) } else { Divider( modifier = Modifier .fillMaxWidth() .height(1.dp), color = Color.Transparent ) Spacer(modifier = Modifier.height(12.dp)) } Icon( currentIcon, contentDescription = tab, modifier = iconModifier, tint = if (isActive) activeBottomTabIconColor else inActiveBottomTabIconColor ) } }, label = { Text( tab, fontSize = 12.sp, color = if (isActive) activeBottomTabIconColor else inActiveBottomTabIconColor ) }, selected = isActive, onClick = { selectedTab = index }, ) } } } }) { AnimatedContent(targetState = selectedTab, transitionSpec = { fadeIn() with fadeOut() }) { targetState -> when (targetState) { 0 -> HomeScreen() 1 -> ListScreen() 2 -> MenuScreen() 3 -> ProfileScreen() 4 -> SettingsScreen() // 4 -> navController.navigate("settings_screen") } } } |
Code explanation:
1. BottomNavigation:
- modifier = Modifier.height(70.dp): Sets the height of the bottom navigation bar to 70dp.
- backgroundColor = Color.White: Sets the background color of the bottom navigation to white.
2. tabs.forEachIndexed:
- tabs.forEachIndexed: Iterates through each tab item using the index and tab name.
- isActive = selectedTab == index: checks if the current tab is selected.
- currentIcon = when(index): Uses when to select the correct drawable icon from resources based on the index.
3. BottomNavigationItem:
- icon: Defines the visual icon shown on the tab.
- modifier = Modifier.wrapContentSize(): Makes the icon container only take as much space as its content.
- horizontalAlignment = Alignment.CenterHorizontally: Centers content (Divider, Spacer, Icon) horizontally inside the column.
- verticalArrangement = Arrangement.Center: Vertically centers all items (Divider, Spacer, Icon) in the column..
- Divider: If the tab is active, it’s colored (e.g., yellow); otherwise, it’s transparent. Visually indicates the selected tab by showing a top border line(Active tab indicator).
- Icon(): Renders the drawable icon.
- label: Displays tab name as text below the icon.
- selected = isActive: Indicates if this tab is currently selected.
- onClick = { selectedTab = index }: Updates the selected tab when this item is clicked.
4. AnimatedContent:
- targetState = selectedTab: Animates the UI change when the tab selection changes.
- transitionSpec = { fadeIn() with fadeOut() }: Specifies that the transition should fade out the old screen and fade in the new one.
- when(targetState): Switches to the corresponding screen (HomeScreen, ListScreen, etc.) based on the selected index.
Complete source code of App.kt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
package com.app.test import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.with import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.BottomNavigation import androidx.compose.material.BottomNavigationItem import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.app.test.screens.HomeScreen import com.app.test.screens.ListScreen import com.app.test.screens.MenuScreen import com.app.test.screens.ProfileScreen import com.app.test.screens.SettingsScreen import org.jetbrains.compose.resources.painterResource import testapp.composeapp.generated.resources.Res import testapp.composeapp.generated.resources.home_icon import testapp.composeapp.generated.resources.list_icon import testapp.composeapp.generated.resources.menu_icon import testapp.composeapp.generated.resources.profile_icon import testapp.composeapp.generated.resources.settings_icon @OptIn(ExperimentalAnimationApi::class) @Composable fun App() { MaterialTheme { var selectedTab by remember { mutableStateOf(0) } val iconModifier = Modifier.size(width = 28.dp, height = 24.dp) val activeBottomTabIconColor = Color(0xFFEEBA00) val inActiveBottomTabIconColor = Color(0XFF283d64) val home = "Home" val list = "List" val menu = "Menu" val profile = "Profile" val settings = "Settings" val tabs = remember() { listOf( home, list, menu, profile, settings ) } Scaffold(bottomBar = { Column { BottomNavigation( modifier = Modifier.height(70.dp), backgroundColor = Color.White ) { tabs.forEachIndexed { index, tab -> val isActive = selectedTab == index val currentIcon = when (index) { 0 -> painterResource(Res.drawable.home_icon) 1 -> painterResource(Res.drawable.list_icon) 2 -> painterResource(Res.drawable.menu_icon) 3 -> painterResource(Res.drawable.profile_icon) 4 -> painterResource(Res.drawable.settings_icon) else -> throw IllegalArgumentException("Unknown tab: $tab") } BottomNavigationItem( icon = { Column( modifier = Modifier .wrapContentSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (isActive) { Divider( modifier = Modifier .fillMaxWidth() .height(1.dp), color = activeBottomTabIconColor ) Spacer(modifier = Modifier.height(12.dp)) } else { Divider( modifier = Modifier .fillMaxWidth() .height(1.dp), color = Color.Transparent ) Spacer(modifier = Modifier.height(12.dp)) } Icon( currentIcon, contentDescription = tab, modifier = iconModifier, tint = if (isActive) activeBottomTabIconColor else inActiveBottomTabIconColor ) } }, label = { Text( tab, fontSize = 12.sp, color = if (isActive) activeBottomTabIconColor else inActiveBottomTabIconColor ) }, selected = isActive, onClick = { selectedTab = index }, ) } } } }) { AnimatedContent(targetState = selectedTab, transitionSpec = { fadeIn() with fadeOut() }) { targetState -> when (targetState) { 0 -> HomeScreen() 1 -> ListScreen() 2 -> MenuScreen() 3 -> ProfileScreen() 4 -> SettingsScreen() // 4 -> navController.navigate("settings_screen") } } } } } |
Screenshot on an Android device:
Screenshot on an iOS device:
Happy coding. Hope you liked my tutorial. If you have any questions in mind, please feel free to ask in the comment section.