ExposedDropdownMenuBox

Functions summary

Unit
@ExperimentalMaterial3Api
@Composable
ExposedDropdownMenuBox(
    expanded: Boolean,
    onExpandedChange: (Boolean) -> Unit,
    modifier: Modifier,
    content: @Composable ExposedDropdownMenuBoxScope.() -> Unit
)

Material Design exposed dropdown menu

Cmn

Functions

ExposedDropdownMenuBox

@ExperimentalMaterial3Api
@Composable
fun ExposedDropdownMenuBox(
    expanded: Boolean,
    onExpandedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable ExposedDropdownMenuBoxScope.() -> Unit
): Unit

Material Design exposed dropdown menu

Menus display a list of choices on a temporary surface. They appear when users interact with a button, action, or other control.

Exposed dropdown menus, sometimes also called "spinners" or "combo boxes", display the currently selected item in a text field to which the menu is anchored. In some cases, it can accept and display user input (whether or not it’s listed as a menu choice), in which case it may be used to implement autocomplete.

Exposed dropdown menu
image

The ExposedDropdownMenuBox is expected to contain a TextField (or OutlinedTextField) and ExposedDropdownMenu as content. The menuAnchor modifier should be passed to the text field.

An example of a read-only Exposed Dropdown Menu:

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier

val options: List<String> = SampleData.take(5)
var expanded by remember { mutableStateOf(false) }
val textFieldState = rememberTextFieldState(options[0])
var checkedIndex: Int? by remember { mutableStateOf(null) }

ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
    TextField(
        // The `menuAnchor` modifier must be passed to the text field to handle
        // expanding/collapsing the menu on click. A read-only text field has
        // the anchor type `PrimaryNotEditable`.
        modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
        state = textFieldState,
        readOnly = true,
        lineLimits = TextFieldLineLimits.SingleLine,
        label = { Text("Label") },
        trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
        colors = ExposedDropdownMenuDefaults.textFieldColors(),
    )
    ExposedDropdownMenu(
        expanded = expanded,
        onDismissRequest = { expanded = false },
        containerColor = MenuDefaults.groupStandardContainerColor,
        shape = MenuDefaults.standaloneGroupShape,
    ) {
        val optionCount = options.size
        options.forEachIndexed { index, option ->
            DropdownMenuItem(
                shapes = MenuDefaults.itemShape(index, optionCount),
                text = { Text(option, style = MaterialTheme.typography.bodyLarge) },
                selected = index == checkedIndex,
                onClick = {
                    textFieldState.setTextAndPlaceCursorAtEnd(option)
                    checkedIndex = index
                },
                selectedLeadingIcon = {
                    Icon(
                        Icons.Filled.Check,
                        modifier = Modifier.size(MenuDefaults.LeadingIconSize),
                        contentDescription = null,
                    )
                },
                contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
            )
        }
    }
}

An example of an editable Exposed Dropdown Menu:

import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

val options: List<String> = SampleData
val textFieldState = rememberTextFieldState()

// The text that the user inputs into the text field can be used to filter the options.
// This sample uses string subsequence matching.
val filteredOptions = options.filteredBy(textFieldState.text)

val (allowExpanded, setExpanded) = remember { mutableStateOf(false) }
val expanded = allowExpanded && filteredOptions.isNotEmpty()

ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = setExpanded) {
    TextField(
        // The `menuAnchor` modifier must be passed to the text field to handle
        // expanding/collapsing the menu on click. An editable text field has
        // the anchor type `PrimaryEditable`.
        modifier =
            Modifier.width(280.dp).menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable),
        state = textFieldState,
        lineLimits = TextFieldLineLimits.SingleLine,
        label = { Text("Label") },
        trailingIcon = {
            ExposedDropdownMenuDefaults.TrailingIcon(
                expanded = expanded,
                // If the text field is editable, it is recommended to make the
                // trailing icon a `menuAnchor` of type `SecondaryEditable`. This
                // provides a better experience for certain accessibility services
                // to choose a menu option without typing.
                modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.SecondaryEditable),
            )
        },
        colors = ExposedDropdownMenuDefaults.textFieldColors(),
    )
    ExposedDropdownMenu(
        modifier = Modifier.heightIn(max = 280.dp),
        expanded = expanded,
        onDismissRequest = { setExpanded(false) },
    ) {
        filteredOptions.forEach { option ->
            DropdownMenuItem(
                text = { Text(option, style = MaterialTheme.typography.bodyLarge) },
                onClick = {
                    textFieldState.setTextAndPlaceCursorAtEnd(option.text)
                    setExpanded(false)
                },
                contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
            )
        }
    }
}

An example of an editable Exposed Dropdown Menu used like a MultiAutoCompleteTextView:

import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.insert
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.substring
import androidx.compose.ui.unit.dp

/**
 * Returns the TextRange of the current token around the cursor, where commas define token
 * boundaries.
 */
fun TextFieldState.currentTokenRange(): TextRange? {
    if (!selection.collapsed) return null

    val cursor = selection.start
    var start = cursor
    while (start > 0 && text[start - 1] != ',') {
        start--
    }
    while (start < cursor && text[start] == ' ') {
        start++
    }

    var end = cursor
    while (end < text.length && text[end] != ',') {
        end++
    }
    return TextRange(start, end)
}

fun TextFieldState.replaceThenAddComma(start: Int, end: Int, text: CharSequence) = edit {
    replace(start, end, text)
    val afterText = start + text.length
    if (afterText == this.length || this.charAt(afterText) != ',') {
        insert(afterText, ", ")
        placeCursorBeforeCharAt(afterText + 2)
    } else {
        placeCursorAfterCharAt(afterText)
    }
}

val allOptions: List<String> = SampleData
val textFieldState = rememberTextFieldState()
val tokenSelection = textFieldState.currentTokenRange()
val tokenAtCursor =
    if (tokenSelection != null) textFieldState.text.substring(tokenSelection) else ""
val filteredOptions =
    if (tokenAtCursor.isBlank()) emptyList() else allOptions.filteredBy(tokenAtCursor)

val (allowExpanded, setExpanded) = remember { mutableStateOf(false) }
val expanded = allowExpanded && filteredOptions.isNotEmpty()

ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = setExpanded) {
    TextField(
        modifier =
            Modifier.width(280.dp).menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable),
        state = textFieldState,
        lineLimits = TextFieldLineLimits.SingleLine,
        label = { Text("Label") },
        trailingIcon = {
            ExposedDropdownMenuDefaults.TrailingIcon(
                expanded = expanded,
                modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.SecondaryEditable),
            )
        },
        colors = ExposedDropdownMenuDefaults.textFieldColors(),
    )
    ExposedDropdownMenu(
        modifier = Modifier.heightIn(max = 280.dp),
        expanded = expanded,
        onDismissRequest = { setExpanded(false) },
    ) {
        filteredOptions.forEach { option ->
            DropdownMenuItem(
                text = { Text(option, style = MaterialTheme.typography.bodyLarge) },
                onClick = {
                    if (tokenSelection != null) {
                        textFieldState.replaceThenAddComma(
                            tokenSelection.start,
                            tokenSelection.end,
                            option,
                        )
                    }
                },
                contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
            )
        }
    }
}
Parameters
expanded: Boolean

whether the menu is expanded or not

onExpandedChange: (Boolean) -> Unit

called when the exposed dropdown menu is clicked and the expansion state changes.

modifier: Modifier = Modifier

the Modifier to be applied to this ExposedDropdownMenuBox

content: @Composable ExposedDropdownMenuBoxScope.() -> Unit

the content of this ExposedDropdownMenuBox, typically a TextField and an ExposedDropdownMenu.