What You Will Build

A dropdown menu that expands and collapses with smooth animations using AnimatedVisibility. Each option has an icon, title, and subtitle. The chevron arrow rotates to indicate open/close state. This replaces the default Material DropdownMenu when you need a persistent, inline selector with rich item layouts.

Why This Pattern Matters

Standard dropdown menus in Material Design are popup-based and hard to style. An expanded dropdown embedded in the layout gives you full control over animation, layout, and theming. The AnimatedVisibility with expandVertically creates a natural accordion effect.

Step 1: Define the Option Model

data class DropdownOption(
    val icon: ImageVector,
    val title: String,
    val subtitle: String
)

Step 2: Build the Animated Dropdown

@Composable
fun ExpandedDropdownScreen() {
    val options = listOf(
        DropdownOption(Icons.Default.Person, "Account", "Manage your account"),
        DropdownOption(Icons.Default.Settings, "Settings", "App preferences"),
        DropdownOption(Icons.Default.Notifications, "Notifications", "Alert settings"),
        DropdownOption(Icons.Default.Lock, "Privacy", "Privacy controls"),
        DropdownOption(Icons.Default.Help, "Help", "Get support")
    )
    var selected by remember { mutableStateOf(options[0]) }
    var expanded by remember { mutableStateOf(false) }
    val rotation by animateFloatAsState(
        targetValue = if (expanded) 180f else 0f,
        animationSpec = tween(300), label = "arrow"
    )

    Column(
        modifier = Modifier
            .width(300.dp)
            .shadow(8.dp, RoundedCornerShape(16.dp))
            .clip(RoundedCornerShape(16.dp))
            .background(Color.White)
    ) {
        // Selected item header
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable { expanded = !expanded }
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(selected.icon, null, tint = Color(0xFF6C63FF),
                modifier = Modifier.size(28.dp))
            Spacer(modifier = Modifier.width(12.dp))
            Column(modifier = Modifier.weight(1f)) {
                Text(selected.title, fontWeight = FontWeight.Bold)
                Text(selected.subtitle, color = Color.Gray, fontSize = 12.sp)
            }
            Icon(
                Icons.Default.KeyboardArrowDown, "Expand",
                modifier = Modifier.rotate(rotation)
            )
        }

        // Expandable options list
        AnimatedVisibility(
            visible = expanded,
            enter = expandVertically() + fadeIn(),
            exit = shrinkVertically() + fadeOut()
        ) {
            Column {
                HorizontalDivider(color = Color.Gray.copy(alpha = 0.1f))
                options.filter { it != selected }.forEach { option ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable {
                                selected = option
                                expanded = false
                            }
                            .padding(16.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Icon(option.icon, null, tint = Color.Gray,
                            modifier = Modifier.size(24.dp))
                        Spacer(modifier = Modifier.width(12.dp))
                        Column {
                            Text(option.title, fontWeight = FontWeight.Medium)
                            Text(option.subtitle, color = Color.Gray, fontSize = 12.sp)
                        }
                    }
                }
            }
        }
    }
}

Tips and Pitfalls

  • Combine expandVertically + fadeIn for a polished accordion effect. Using only one looks abrupt.
  • Filter out the selected item from the options list so it only shows in the header, not duplicated below.
  • Arrow rotation of 180 degrees flips the chevron cleanly. Use tween(300) to keep it in sync with the expand animation.
  • Shadow + clip order matters. Apply .shadow() before .clip() so the shadow renders outside the clipped bounds.

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.