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.