What You Will Build

A custom bottom tab bar where the selected tab bounces upward with a spring animation, scales up, and shows a colored circular highlight. Each tab has its own accent color and the content area cross-fades between pages. This replaces the standard BottomNavigation with a more interactive, playful navigation experience.

Why Custom Tab Bars Require Animation Knowledge

Custom tab bars combine several animation techniques: animateFloatAsState for scale, animateDpAsState for vertical offset, spring physics for bouncy feel, and AnimatedContent for page transitions. Mastering these together gives you the tools for any micro-interaction in Compose.

Key Compose Concepts

  • animateFloatAsState with spring for bouncy scale animations.
  • animateDpAsState for smooth vertical offset transitions.
  • AnimatedContent for cross-fading between page content.
  • MutableInteractionSource with indication = null to remove the default ripple effect.

Step 1: Define Tabs with Colors

val tabs = listOf(
    Triple("Home", Icons.Default.Home, Color(0xFF4A90D9)),
    Triple("Explore", Icons.Default.Explore, Color(0xFF50C878)),
    Triple("Notifications", Icons.Default.Notifications, Color(0xFFFF8C00)),
    Triple("Profile", Icons.Default.Person, Color(0xFF9B59B6))
)
var selected by remember { mutableIntStateOf(0) }

Step 2: Animated Tab Item

tabs.forEachIndexed { index, (title, icon, color) ->
    val isSelected = selected == index
    val scale by animateFloatAsState(
        targetValue = if (isSelected) 1.15f else 1f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy
        ),
        label = "scale"
    )
    val offsetY by animateDpAsState(
        targetValue = if (isSelected) (-4).dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy
        ),
        label = "offset"
    )

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .scale(scale)
            .offset(y = offsetY)
            .clickable(
                interactionSource = remember { MutableInteractionSource() },
                indication = null
            ) { selected = index }
            .padding(horizontal = 12.dp, vertical = 6.dp)
    ) {
        if (isSelected) {
            Box(
                modifier = Modifier
                    .size(36.dp)
                    .clip(CircleShape)
                    .background(color.copy(alpha = 0.2f)),
                contentAlignment = Alignment.Center
            ) {
                Icon(icon, title, tint = color,
                    modifier = Modifier.size(20.dp))
            }
        } else {
            Icon(icon, title, tint = Color.Gray,
                modifier = Modifier.size(22.dp))
        }
        Spacer(Modifier.height(2.dp))
        Text(
            title, fontSize = 10.sp,
            fontWeight = if (isSelected) FontWeight.Bold
                else FontWeight.Normal,
            color = if (isSelected) color else Color.Gray
        )
    }
}

Step 3: Animated Content Area

AnimatedContent(
    targetState = selected,
    transitionSpec = { fadeIn() togetherWith fadeOut() },
    label = "content"
) { idx ->
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Icon(tabs[idx].second, null,
            modifier = Modifier.size(80.dp),
            tint = tabs[idx].third)
        Spacer(Modifier.height(16.dp))
        Text(tabs[idx].first, fontSize = 28.sp,
            fontWeight = FontWeight.Bold)
    }
}

Tips and Pitfalls

  • Spring.DampingRatioMediumBouncy creates a playful bounce. Use DampingRatioLowBouncy for more bounce or DampingRatioNoBouncy for smooth transitions.
  • indication = null removes the default Material ripple, which looks wrong on custom animated tab items.
  • CircleShape background with 0.2f alpha on selected icons creates a soft highlight without overwhelming the icon.
  • Use Scaffold bottomBar to position the tab bar correctly with safe area insets handling.

Minimum SDK: API 24+ with Compose BOM 2024.01+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.