OverlayDropdownMenu
OverlayDropdownMenu is a BasicComponent wrapper that opens an OverlayDropdownPopup when clicked. Unlike OverlayDropdownPreference, it does not own a single selection index — selection state lives entirely on each DropdownItem's selected and onClick. Use it for action menus, multi-select menus, or any case where the items in a popup do not share one mutually exclusive choice.
Prerequisite
This component depends on Scaffold providing COUIPopupHost to render popup content. It must be used within Scaffold, otherwise popup content will not render correctly.
Import
import io.github.suqi8.coui.kmp.menu.OverlayDropdownMenu
import io.github.suqi8.coui.kmp.basic.DropdownEntry
import io.github.suqi8.coui.kmp.basic.DropdownItemBasic Usage
Wrap a single DropdownEntry to render a basic dropdown menu row:
var selectedIndex by remember { mutableStateOf(0) }
val entry = DropdownEntry(
items = listOf("Option 1", "Option 2", "Option 3").mapIndexed { index, text ->
DropdownItem(
text = text,
selected = selectedIndex == index,
onClick = { selectedIndex = index },
)
}
)
Scaffold {
OverlayDropdownMenu(
title = "Dropdown Menu",
entry = entry
)
}Grouped Menu
Pass a List<DropdownEntry> to render multiple groups separated by dividers. By default, collapseOnSelection is entries.size <= 1, so multi-group menus stay open after each selection.
var sizeIndex by remember { mutableStateOf(0) }
var colorIndex by remember { mutableStateOf(0) }
val entries = listOf(
DropdownEntry(
items = listOf("Small", "Medium").mapIndexed { index, text ->
DropdownItem(text = text, selected = sizeIndex == index, onClick = { sizeIndex = index })
}
),
DropdownEntry(
items = listOf("Red", "Green", "Blue").mapIndexed { index, text ->
DropdownItem(text = text, selected = colorIndex == index, onClick = { colorIndex = index })
}
)
)
Scaffold {
OverlayDropdownMenu(
title = "Grouped Menu",
entries = entries,
collapseOnSelection = false
)
}Multi Select
Selection state lives on DropdownItem.selected, so multiple items can be selected simultaneously by toggling each item's value from onClick.
var selected by remember { mutableStateOf(setOf("A1", "B2")) }
val entries = listOf(
DropdownEntry(
items = listOf("A1", "A2").map { text ->
DropdownItem(
text = text,
selected = text in selected,
onClick = {
selected = if (text in selected) selected - text else selected + text
}
)
}
),
DropdownEntry(
items = listOf("B1", "B2", "B3").map { text ->
DropdownItem(
text = text,
selected = text in selected,
onClick = {
selected = if (text in selected) selected - text else selected + text
}
)
}
)
)
Scaffold {
OverlayDropdownMenu(
title = "Multi Select Menu",
entries = entries,
collapseOnSelection = false
)
}Items with Icons and Summaries
Each DropdownItem can display a leading icon and a summary line below its text. The icon lambda receives a pre-sized Modifier that should be applied to the icon composable.
val entry = DropdownEntry(
items = listOf(
DropdownItem(
text = "Rename",
summary = "Change the display name",
icon = { modifier ->
Icon(
modifier = modifier,
imageVector = COUIIcons.Rename,
contentDescription = null,
)
},
onClick = { /* handle action */ },
),
DropdownItem(text = "Delete", onClick = { /* handle action */ }),
)
)
Scaffold {
OverlayDropdownMenu(
title = "Item Icons",
entry = entry
)
}Observe Expanded State
var expanded by remember { mutableStateOf(false) }
val entry = DropdownEntry(
items = listOf("Option 1", "Option 2", "Option 3").map { DropdownItem(text = it) }
)
Scaffold {
OverlayDropdownMenu(
title = "Observe Expanded",
summary = if (expanded) "Expanded" else "Collapsed",
entry = entry,
onExpandedChange = { expanded = it }
)
}Items with a Hint Slot
The hint slot renders between the title block and the selection indicator, capped at 40dp wide. It suits a red dot, a count badge, or a very short label. Matching ColorOS, the hint is hidden entirely while the row is disabled.
val entry = DropdownEntry(
items = listOf(
DropdownItem(text = "Inbox", hint = { Badge(count = 12) }),
DropdownItem(text = "Updates", hint = { Badge() }),
// The badge is suppressed because the row is disabled.
DropdownItem(text = "Archive", hint = { Badge(count = 3) }, enabled = false),
)
)
Scaffold {
OverlayDropdownMenu(title = "Hints", entry = entry)
}Group Headers
A DropdownEntry can declare a title, rendered above its items as a non-clickable header row (12sp medium, secondary label color, at most 2 lines). Headers coexist with the group divider that already separates adjacent entries.
val entries = listOf(
DropdownEntry(
title = "Sort by",
items = listOf("Name", "Date modified").map { DropdownItem(text = it) }
),
DropdownEntry(
title = "Order",
items = listOf("Ascending", "Descending").map { DropdownItem(text = it) }
)
)
Scaffold {
OverlayDropdownMenu(title = "Group Headers", entries = entries)
}Alert Items
Set alert = true to mark a destructive action. Its title uses the error color instead of the normal label color; disabled alert rows still fall back to the disabled color.
val entry = DropdownEntry(
items = listOf(
DropdownItem(text = "Rename"),
DropdownItem(text = "Delete", alert = true),
)
)
Scaffold {
OverlayDropdownMenu(title = "Alert Item", entry = entry)
}Component States
Disabled State
OverlayDropdownMenu(
title = "Disabled Menu",
summary = "This menu is currently unavailable",
entry = DropdownEntry(items = listOf(DropdownItem(text = "Option 1"))),
enabled = false
)The menu is also implicitly disabled when no DropdownEntry contains any items.
Disabled Items
Individual items can be disabled via DropdownItem.enabled, and an entire group via DropdownEntry.enabled. Disabled rows are grayed out and ignore clicks.
val entries = listOf(
DropdownEntry(
items = listOf(
DropdownItem(text = "Available option"),
DropdownItem(text = "Unavailable option", enabled = false),
)
),
DropdownEntry(
items = listOf(DropdownItem(text = "Whole group disabled")),
enabled = false
)
)
Scaffold {
OverlayDropdownMenu(
title = "Partially Disabled",
entries = entries
)
}Properties
OverlayDropdownMenu Properties (Entries Overload)
| Property Name | Type | Description | Default Value | Required |
|---|---|---|---|---|
| entries | List<DropdownEntry> | Dropdown entry groups separated by dividers | - | Yes |
| title | String | Title of the menu row | - | Yes |
| modifier | Modifier | Modifier applied to the component | Modifier | No |
| titleColor | BasicComponentColors | Title text color configuration | BasicComponentDefaults.titleColor() | No |
| summary | String? | Summary description of the menu | null | No |
| summaryColor | BasicComponentColors | Summary text color configuration | BasicComponentDefaults.summaryColor() | No |
| dropdownColors | DropdownColors | Color configuration for dropdown items | DropdownDefaults.dropdownColors() | No |
| startAction | @Composable (() -> Unit)? | Custom start side content | null | No |
| bottomAction | @Composable (() -> Unit)? | Custom bottom side content | null | No |
| insideMargin | PaddingValues | Internal content padding | BasicComponentDefaults.InsideMargin | No |
| maxHeight | Dp? | Maximum height of the dropdown popup | null | No |
| enabled | Boolean | Whether component is interactive | true | No |
| renderInRootScaffold | Boolean | Whether to render the popup in the root (outermost) Scaffold. When true, the popup covers the full screen. When false, it renders within the current Scaffold's bounds with position compensation | true | No |
| collapseOnSelection | Boolean | Whether to close the popup after each selection | entries.size <= 1 | No |
| onExpandedChange | ((Boolean) -> Unit)? | Callback when the expanded state changes | null | No |
Entry Overload Properties
| Property Name | Type | Description | Default Value | Required |
|---|---|---|---|---|
| entry | DropdownEntry | Single dropdown entry group | - | Yes |
| collapseOnSelection | Boolean | Whether to close the popup after selection | true | No |
All other parameters are identical to the entries overload above.
DropdownEntry Properties
| Property Name | Type | Description | Default Value | Required |
|---|---|---|---|---|
| items | List<DropdownItem> | Items shown in this dropdown group | - | Yes |
| enabled | Boolean | Whether this group is enabled. False disables all items; true still respects each item's enabled state | true | No |
| title | String? | Optional non-clickable group header rendered above the items (12sp medium, secondary label, max 2 lines) | null | No |
DropdownItem Properties
| Property Name | Type | Description | Default Value | Required |
|---|---|---|---|---|
| text | String | Text shown for the item | - | Yes |
| enabled | Boolean | Whether the item can be clicked. Disabled items are gray | true | No |
| selected | Boolean | Whether the item is selected | false | No |
| onClick | (() -> Unit)? | Callback invoked when the item is clicked | null | No |
| icon | @Composable ((Modifier) -> Unit)? | Icon shown before the item text | null | No |
| summary | String? | Summary text shown below the item text | null | No |
| children | List<DropdownItem>? | Optional submenu items; cascading variants only | null | No |
| hint | @Composable (() -> Unit)? | Optional trailing hint slot (badge, red dot, short count) shown before the selection indicator, width-capped at 40dp. Hidden entirely while the row is disabled | null | No |
| alert | Boolean | Whether this is an alert (destructive) item; its title uses the error color | false | No |
DropdownColors Properties
| Property Name | Type | Description |
|---|---|---|
| contentColor | Color | Color of the option title |
| summaryColor | Color | Color of the option summary |
| containerColor | Color | Background color of the option |
| selectedContentColor | Color | Title color of the selected option |
| selectedSummaryColor | Color | Summary color of the selected option |
| selectedContainerColor | Color | Background color of the selected option |
| selectedIndicatorColor | Color | Color of the selected indicator icon |
| disabledContentColor | Color | Title color of a disabled option |
| alertContentColor | Color | Title color of an alert option |
| headerColor | Color | Title color of a group header row |

