Selection¶
SelectionList presents a single- or multiple-choice list inside a modal bottom
sheet, with optional search and inline two-level disclosure. Rows come from a
SelectionListNode tree: a node with no children is a leaf that selects on tap, while
a node with children is an expandable parent that reveals its children inline.
The list is controlled — it reflects the selection you pass and reports taps through a
callback; it never mutates the selection or dismisses itself. Selected rows show a
check (from AppTheme.icons.check), and a collapsed parent lists its selected children
as its subtitle. The disclosure chevron uses AppTheme.icons.expand, so provide real
icons (for example via defaultAppIcons()) when constructing your theme.
Components¶
SelectionList— single-choice (selectedId) and multiple-choice (selectedIds) overloads, presented as a modal bottom sheet.SelectionListContent— the embeddable list body without the surrounding sheet, for inline use.SelectionListNode— an immutable tree node:id,title, optionalsubtitle/leadingGlyph, andchildren.

Single choice¶
With no confirmButton, tapping an item updates the selection and dismisses the
sheet — just update your state in onSelect:
if (showSheet) {
SelectionList(
title = "Category",
nodes = nodes,
selectedId = choice,
isSearchable = true,
onSelect = { choice = it }, // sheet dismisses on tap
onDismissRequest = { showSheet = false },
)
}
Pass a confirmButton to keep the sheet open while the user revises their choice;
then only the confirm control dismisses:
SelectionList(
title = "Category",
nodes = nodes,
selectedId = choice,
onSelect = { choice = it }, // stays open
onDismissRequest = { showSheet = false },
confirmButton = { TextButton(text = doneLabel, onClick = { showSheet = false }) },
)
Multiple choice¶
if (showSheet) {
SelectionList(
title = "Categories",
nodes = nodes,
selectedIds = choices,
onSelect = { id -> // toggle membership; the sheet stays open
choices = if (id in choices) choices - id else choices + id
},
onDismissRequest = { showSheet = false },
confirmButton = { TextButton(text = doneLabel, onClick = { showSheet = false }) },
)
}
Confirm button slot¶
confirmButton is an optional composable slot shown at the trailing edge of the
header. You supply the content and wire its onClick, so it can be a localized text
button, an icon, or an image:
confirmButton = { TextButton(text = stringResource(R.string.done), onClick = { showSheet = false }) }
// or an icon / image:
confirmButton = { IconButton(icon = AppTheme.icons.check, contentDescription = doneLabel, onClick = { showSheet = false }) }
It is available on both the single- and multiple-choice overloads. Multiple choice should provide it (rows don't dismiss on tap); single choice usually omits it and dismisses on selection, but may include it when you want an explicit confirm step.
Inline embedding¶
Use SelectionListContent to embed the list directly in a screen without a sheet:
SelectionListContent(
title = "Category",
nodes = nodes,
selectedIds = setOf(choice),
onSelect = { choice = it },
)
Building the node tree¶
val nodes = listOf(
SelectionListNode(id = "water", title = "Water"), // leaf
SelectionListNode(
id = "fruit",
title = "Fruit",
leadingGlyph = "🍎",
children = listOf( // expands inline
SelectionListNode(id = "apple", title = "Apple"),
SelectionListNode(id = "banana", title = "Banana"),
),
),
)
Identifiers must be unique across the whole tree; they are used as the selection
values reported by the list. When isSearchable is true, the search field filters
across both levels (case-insensitive) and force-expands matching parents.