What You Will Build

An expandable Floating Action Button (FAB) that reveals multiple action buttons when tapped. The main FAB rotates its icon while child buttons scale in with staggered delays. This is the speed-dial pattern used in Gmail, Google Maps, and many productivity apps for quick multi-action access.

Why This Pattern Matters

The expandable FAB keeps the primary action prominent while hiding secondary options until needed. It reduces UI clutter without sacrificing functionality. Compose makes this particularly elegant because animateFloatAsState handles all the transition math automatically.

Key Compose Concepts

  • animateFloatAsState with spring physics for natural motion
  • Staggered tween delays per child button index
  • Modifier.rotate() to spin the plus icon into an X

Step 1: Define the Action Data Class

data class FABAction(
    val icon: ImageVector,
    val label: String,
    val onClick: () -> Unit
)

Step 2: Build the Expandable FAB

@Composable
fun FABMenu(actions: List<FABAction>, modifier: Modifier = Modifier) {
    var showButtons by remember { mutableStateOf(false) }
    val rotation by animateFloatAsState(
        targetValue = if (showButtons) -45f else 0f,
        animationSpec = spring(dampingRatio = 0.6f),
        label = "rotation"
    )

    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Bottom
    ) {
        actions.forEachIndexed { index, action ->
            val scale by animateFloatAsState(
                targetValue = if (showButtons) 1f else 0f,
                animationSpec = tween(
                    durationMillis = 200,
                    delayMillis = index * 100
                ),
                label = "scale_$index"
            )

            if (scale > 0.01f) {
                Box(
                    modifier = Modifier
                        .padding(bottom = 8.dp)
                        .scale(scale)
                        .size(50.dp)
                        .background(Color.Gray.copy(alpha = 0.3f), CircleShape)
                        .clickable {
                            action.onClick()
                            showButtons = false
                        },
                    contentAlignment = Alignment.Center
                ) {
                    Icon(
                        imageVector = action.icon,
                        contentDescription = action.label,
                        modifier = Modifier.size(24.dp)
                    )
                }
            }
        }

        Spacer(modifier = Modifier.height(8.dp))

        Box(
            modifier = Modifier
                .size(56.dp)
                .shadow(8.dp, CircleShape)
                .background(Color.White, CircleShape)
                .clickable { showButtons = !showButtons },
            contentAlignment = Alignment.Center
        ) {
            Icon(
                imageVector = Icons.Default.Add,
                contentDescription = "Menu",
                modifier = Modifier
                    .size(30.dp)
                    .rotate(rotation),
                tint = Color.Black
            )
        }
    }
}

Step 3: Wire It into a Screen

@Composable
fun FABScreen() {
    val actions = listOf(
        FABAction(Icons.Default.Language, "Globe") {},
        FABAction(Icons.Default.Palette, "Palette") {},
        FABAction(Icons.Default.Description, "Document") {}
    )

    Box(modifier = Modifier.fillMaxSize()) {
        FABMenu(
            actions = actions,
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(30.dp)
        )
    }
}

Tips and Pitfalls

  • Spring damping ratio of 0.6f gives the rotation a slight bounce that feels organic rather than mechanical.
  • Check scale > 0.01f before composing child buttons so they are removed from the tree when hidden, saving composition cost.
  • Dismiss on outside tap: Wrap the whole screen in a Box with a transparent clickable background that sets showButtons = false.
  • Accessibility: Add contentDescription to every icon and consider semantics { role = Role.Button } for the parent FAB.

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.