Skip to content

Curved Bottom Bar in Jetpack Compose

Today I’ll show you how to create a curved bottom bar in Jetpack Compose.

To start let’s create a class that defines a route.

data class Screen(
    val route: String,
    @DrawableRes val icon: Int,
    @DrawableRes val selectedIcon: Int,
)

I’m using icon and selectedIcon because in my implementation they change but you can change the tint instead of whatever you want.

Now let’s define the bottom bar.

@Composable
fun BottomMenuBar(
    screens: List<Screen>,
    currentScreen: Screen?,
    onNavigateTo: (Screen) -> Unit,
)

screens is the routes you want to show in the bottom bar. You need to specify 5 screens for this bottom bar to work.

currentScreen is the currently selected screen so we can change the icon.

onNavigateTo is called when the icon for a screen is tapped in the bottom bar.

Before we continue let’s build the rounded shape that’s used for the background.

private fun menuBarShape() = GenericShape { size, _ ->
    reset()

    moveTo(0f, 0f)

    val width = 150f
    val height = 90f

    val point1 = 75f
    val point2 = 85f

    lineTo(size.width / 2 - width, 0f)

    cubicTo(
        size.width / 2 - point1, 0f,
        size.width / 2 - point2, height,
        size.width / 2, height
    )

    cubicTo(
        size.width / 2 + point2, height,
        size.width / 2 + point1, 0f,
        size.width / 2 + width, 0f
    )

    lineTo(size.width / 2 + width, 0f)

    lineTo(size.width, 0f)
    lineTo(size.width, size.height)
    lineTo(0f, size.height)

    close()
}

We use cubic bezier curves to get that rounded shape. If you want to change the rounded part you can change width, height, point1 and point2. By changing these variables the curvature will change.

Now let’s define the preview so we can see the bottom bar as we change it.

@Preview(showSystemUi = true)
@Composable
private fun Preview() {
    var currentScreen by remember { mutableStateOf<Screen?>(null) }

    Box(
        contentAlignment = Alignment.BottomCenter,
        modifier = Modifier.fillMaxSize()
    ) {
        BottomMenuBar(
            screens = listOf(
                Screen(
                    route = "home",
                    icon = R.drawable.outline_home_24,
                    selectedIcon = R.drawable.baseline_home_24,
                ),
                Screen(
                    route = "products",
                    icon = R.drawable.outline_collections_24,
                    selectedIcon = R.drawable.baseline_collections_24,
                ),
                Screen(
                    route = "cart",
                    icon = R.drawable.outline_shopping_cart_24,
                    selectedIcon = R.drawable.baseline_shopping_cart_24,
                ),
                Screen(
                    route = "profile",
                    icon = R.drawable.outline_person_24,
                    selectedIcon = R.drawable.baseline_person_24,
                ),
                Screen(
                    route = "chat",
                    icon = R.drawable.outline_chat_24,
                    selectedIcon = R.drawable.baseline_chat_24,
                ),
            ),
            currentScreen = currentScreen,
            onNavigateTo = { currentScreen = it },
        )
    }
}

The currentScreen variable in the preview is used so we can play with the bottom bar and see it changing.

Now let’s actually implement the bottom bar.

@Composable
fun BottomMenuBar(
    screens: List<Screen>,
    currentScreen: Screen?,
    onNavigateTo: (Screen) -> Unit,
) {
    val backgroundShape = remember { menuBarShape() }

    Box {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(56.dp)
                .background(Color.White, backgroundShape)
                .align(Alignment.BottomCenter)
        )

        Column(
            modifier = Modifier
                .align(Alignment.TopCenter)
        ) {
            FloatingActionButton(
                shape = RoundedCornerShape(50),
                containerColor = Color.White,
                contentColor = Color.Gray,
                onClick = {},
                modifier = Modifier.clip(RoundedCornerShape(50))
            ) {
                Row(
                    modifier = Modifier.size(64.dp)
                ) {
                    BottomBarItem(screens[2], currentScreen, onNavigateTo)
                }
            }
            Spacer(modifier = Modifier.height(30.dp))
        }

        Row(
            modifier = Modifier
                .height(56.dp)
                .align(Alignment.BottomCenter)
        ) {
            BottomBarItem(screens[0], currentScreen, onNavigateTo)
            BottomBarItem(screens[1], currentScreen, onNavigateTo)

            Spacer(modifier = Modifier.width(72.dp))

            BottomBarItem(screens[3], currentScreen, onNavigateTo)
            BottomBarItem(screens[4], currentScreen, onNavigateTo)
        }
    }
}

We also need to implement a component for the menu items,

@Composable
private fun RowScope.BottomBarItem(
    screen: Screen,
    currentScreen: Screen?,
    onNavigateTo: (Screen) -> Unit,
) {
    val selected = currentScreen?.route == screen.route

    Box(
        Modifier
            .selectable(
                selected = selected,
                onClick = { onNavigateTo(screen) },
                role = Role.Tab,
                interactionSource = remember { MutableInteractionSource() },
                indication = remember { ripple(radius = 32.dp) }
            )
            .fillMaxHeight()
            .weight(1f),
        contentAlignment = Alignment.Center
    ) {
        BadgedBox(
            badge = {},
            content = {
                Image(
                    painter = painterResource(
                        id = when {
                            selected -> screen.selectedIcon
                            else -> screen.icon
                        }
                    ),
                    contentDescription = null
                )
            },
        )
    }
}

Well, that’s it. You have a working curved bottom bar.


If you have any comments or suggestions, please reach out to me on X.

Photo by CHUTTERSNAP on Unsplash