import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.ChevronDown
import com.composables.icons.lucide.Clipboard
import com.composables.icons.lucide.Copy
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Maximize
import com.composables.icons.lucide.Scissors
import com.composables.icons.lucide.Trash2
import com.composeunstyled.DropdownMenuPanel
import com.composeunstyled.LocalContentColor
import com.composeunstyled.MenuItem
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDropdownMenu
import com.composeunstyled.UnstyledHorizontalSeparator
import com.composeunstyled.UnstyledIcon
@Composable
fun DropdownMenuDemo() {
class DropdownOption(
val text: String,
val icon: ImageVector,
val enabled: Boolean = true,
val dangerous: Boolean = false,
)
val options = listOf(
DropdownOption("Select All", Lucide.Maximize),
DropdownOption("Copy", Lucide.Copy),
DropdownOption("Cut", Lucide.Scissors, enabled = false),
DropdownOption("Paste", Lucide.Clipboard),
DropdownOption("Delete", Lucide.Trash2, dangerous = true),
)
var expanded by remember { mutableStateOf(true) }
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
sideOffset = 4.dp,
panel = {
DropdownMenuPanel(
modifier = Modifier
.width(240.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFF8FAFC))
.border(1.dp, Color(0xFFCACACA), RoundedCornerShape(8.dp)),
enter = scaleIn(
animationSpec = tween(durationMillis = 120, easing = LinearOutSlowInEasing),
initialScale = 0.8f,
transformOrigin = TransformOrigin(0f, 0f),
) + fadeIn(tween(durationMillis = 30)),
exit = scaleOut(
animationSpec = tween(durationMillis = 75),
targetScale = 0.8f,
transformOrigin = TransformOrigin(0f, 0f),
) +
fadeOut(tween(durationMillis = 75)),
) {
Column {
options.forEachIndexed { index, option ->
if (index == 1 || index == options.lastIndex) {
UnstyledHorizontalSeparator(color = Color(0xFFBDBDBD))
}
MenuItem(
onClick = {},
enabled = option.enabled,
modifier = Modifier
.padding(4.dp)
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
.clip(RoundedCornerShape(8.dp))
.fillMaxWidth(),
indication = LocalIndication.current,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
val contentColor = (
if (option.dangerous) {
Color(0xFFDC2626)
} else {
LocalContentColor.current
}
).copy(alpha = if (option.enabled) 1f else 0.5f)
UnstyledIcon(
imageVector = option.icon,
contentDescription = null,
tint = contentColor,
)
Spacer(Modifier.width(12.dp))
BasicText(
text = option.text,
style = TextStyle(color = contentColor),
)
}
}
}
}
}
},
anchor = {
UnstyledButton(
onClick = { expanded = true },
modifier = Modifier
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
.clip(RoundedCornerShape(6.dp))
.background(Color(0xFFF8FAFC))
.border(1.dp, Color(0xFFCACACA), RoundedCornerShape(6.dp)),
indication = LocalIndication.current,
) {
Row(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
BasicText("Options")
Spacer(Modifier.width(8.dp))
UnstyledIcon(Lucide.ChevronDown, null)
}
}
},
)
}Features
- Anchor-based placement
- Keyboard menu navigation
- Auto-dismiss on outside click
- Panel enter and exit transitions
Installation
implementation("com.composables:composeunstyled-dropdown-menu")
Anatomy
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = onExpandedChange,
panel = {
DropdownMenuPanel {
MenuItem(onClick = onClick) {
}
}
},
anchor = {
},
)
Concepts
UnstyledDropdownMenumarks the anchor area and renders the floating menu when expanded.- The
anchorslot renders the content the menu is positioned against. - The
panelslot renders the floating menu content. DropdownMenuPanelrenders the menu surface.MenuItemrenders an item insideDropdownMenuPanel.
Accessibility
Dropdown menu handles keyboard interactions out of the box. Pressing Arrow Down opens the menu and focuses the first item. Pressing Arrow Down and Arrow Up moves focus to the next and previous item. Home moves focus to the first item. End moves focus to the last item. Escape closes the menu. Use MenuItem for focusable menu actions.
Code Examples
Opening and closing a dropdown menu
Use the expanded parameter to show the menu and the onExpandedChange callback to update that state:
var expanded by remember { mutableStateOf(false) }
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
panel = {
DropdownMenuPanel {
MenuItem(onClick = { expanded = false }) {
BasicText("Close")
}
}
},
anchor = {
BasicText(
text = "Open menu",
modifier = Modifier.clickable { expanded = true },
)
},
)
Positioning a dropdown menu
Use the side, alignment, sideOffset, and alignmentOffset parameters to place the menu panel relative to the anchor:
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
side = AnchorSide.Bottom,
alignment = AnchorAlignment.End,
sideOffset = 8.dp,
panel = {
DropdownMenuPanel {
MenuItem(onClick = { select() }) {
BasicText("Item")
}
}
},
anchor = {
BasicText("Open menu")
},
)
Closing the menu after clicking an item
MenuItem closes the menu after click by default by calling the dropdown's onExpandedChange callback with false:
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
panel = {
DropdownMenuPanel {
MenuItem(onClick = { select() }) {
BasicText("Item")
}
}
},
anchor = {
BasicText("Open menu")
},
)
Keeping the menu open after clicking an item
Use the closeOnClick parameter when a menu item should update state without dismissing the menu:
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
panel = {
DropdownMenuPanel {
MenuItem(
closeOnClick = false,
onClick = { enabled = enabled.not() },
) {
BasicText("Toggle option")
}
}
},
anchor = {
BasicText("Open menu")
},
)
Animating the dropdown menu
Use the enter and exit parameters on DropdownMenuPanel to animate the menu panel:
UnstyledDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
panel = {
DropdownMenuPanel(
enter = fadeIn(),
exit = fadeOut(),
) {
MenuItem(onClick = { select() }) {
BasicText("Item")
}
}
},
anchor = {
BasicText("Open menu")
},
)
API Reference
UnstyledDropdownMenu
| Parameter | Type | Description |
|---|---|---|
expanded |
Boolean |
|
onExpandedChange |
(Boolean) -> Unit |
|
modifier |
Modifier |
|
side |
AnchorSide |
|
alignment |
AnchorAlignment |
|
sideOffset |
Dp |
|
alignmentOffset |
Dp |
|
panel |
DropdownMenuScope.() -> Unit |
|
anchor |
() -> Unit |
DropdownMenuScope.DropdownMenuPanel
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
|
enter |
EnterTransition |
|
exit |
ExitTransition |
|
content |
DropdownMenuPanelScope.() -> Unit |
DropdownMenuPanelScope.MenuItem
| Parameter | Type | Description |
|---|---|---|
onClick |
() -> Unit |
|
modifier |
Modifier |
|
enabled |
Boolean |
|
closeOnClick |
Boolean |
|
interactionSource |
MutableInteractionSource? |
|
indication |
Indication? |
|
content |
() -> Unit |