Jetpack Compose:DropdownMenu ignores anchor and shows at bottom of screen in Compose Dialog


I am trying to implement a custom Jetpack Compose Dialog that contains two elements with their own anchored DropdownMenu:

  1. A Header Row with a downward icon that should trigger a menu when clicked.

  2. An OutlinedTextField that should trigger a suggestion menu automatically once the user types more than 3 characters.

The Issue: Despite wrapping each element in a Box to serve as an anchor, the DropdownMenu does not appear below the corresponding element. Instead, it seems to ignore the layout hierarchy and renders at the top-left of the Dialog window (or the screen).

What I've tried:

  • Wrapping the IconButton and TextField in individual Box containers.

  • Using DropdownMenu inside the same parent as the trigger element.

  • Testing with PopupProperties(focusable = false).

Expected Result: Each menu should be anchored to its respective trigger (the icon or the text field) within the Dialog's layout.

Here is my source code

@Composable
fun CustomComposeDialog(onDismiss: () -> Unit) {
    var textInput1 by remember { mutableStateOf("") }
    var textInput2 by remember { mutableStateOf("") }
    
    // Independent states for two different menus
    var headerMenuExpanded by remember { mutableStateOf(false) }
    val textFieldMenuExpanded = textInput1.length > 3

    Dialog(onDismissRequest = onDismiss) {
        // The container for the dialog content
        Surface(
            shape = RoundedCornerShape(16.dp),
            color = MaterialTheme.colorScheme.surface,
            modifier = Modifier.fillMaxWidth().padding(16.dp)
        ) {
            Column(
                modifier = Modifier.padding(20.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                Text(
                    text = "Custom Entry",
                    style = MaterialTheme.typography.headlineSmall
                )

                // --- 1. Header with Icon & its Menu ---
                Box {
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        Text("Category Options")
                        IconButton(onClick = { headerMenuExpanded = true }) {
                            Icon(Icons.Default.ArrowDropDown, contentDescription = null)
                        }
                    }
                    
                    DropdownMenu(
                        expanded = headerMenuExpanded,
                        onDismissRequest = { headerMenuExpanded = false }
                    ) {
                        DropdownMenuItem(text = { Text("Option A") }, onClick = { headerMenuExpanded = false })
                        DropdownMenuItem(text = { Text("Option B") }, onClick = { headerMenuExpanded = false })
                    }
                }

                // --- 2. First Text Field & its Anchored Menu ---
                Box {
                    OutlinedTextField(
                        value = textInput1,
                        onValueChange = { textInput1 = it },
                        label = { Text("Search or Type...") },
                        modifier = Modifier.fillMaxWidth()
                    )

                    // This menu anchors specifically to the TextField Box
                    DropdownMenu(
                        expanded = textFieldMenuExpanded,
                        onDismissRequest = { /* Controlled by text length */ },
                        // Focusable false allows the user to keep typing while menu is open
                        properties = androidx.compose.ui.window.PopupProperties(focusable = false)
                    ) {
                        val suggestions = listOf("Result 1", "Result 2", "Result 3")
                        suggestions.forEach { suggestion ->
                            DropdownMenuItem(
                                text = { Text(suggestion) },
                                onClick = { 
                                    textInput1 = suggestion 
                                    // Note: In a real app, you'd reset a flag here to hide it
                                }
                            )
                        }
                    }
                }

                // --- 3. Second Text Field ---
                OutlinedTextField(
                    value = textInput2,
                    onValueChange = { textInput2 = it },
                    label = { Text("Additional Notes") },
                    modifier = Modifier.fillMaxWidth()
                )

                // Close Button
                TextButton(
                    onClick = onDismiss,
                    modifier = Modifier.align(Alignment.End)
                ) {
                    Text("Close")
                }
            }
        }
    }
}

Here is my current result!

enter image description hereenter image description here

1
Mar 9 at 5:50 PM
User AvatarWisal Muhammad
#android#menu#android-jetpack-compose#dialog

Accepted Answer

Yeah this is a known issue with DropdownMenu inside Dialog. The problem is Dialog creates its own window, and DropdownMenu uses Popup internally which calculates position relative to that window's origin (0,0) instead of your actual anchor. So your Box wrapping doesn't help because the coordinates get lost.

I dealt with this same thing and found two approaches that work:


Option 1: Use AlertDialog instead

AlertDialog handles the window differently and dropdown anchoring just works inside it.

@Composable
fun CustomComposeDialog(onDismiss: () -> Unit) {
    var textInput1 by remember { mutableStateOf("") }
    var textInput2 by remember { mutableStateOf("") }
    var headerMenuExpanded by remember { mutableStateOf(false) }
    val textFieldMenuExpanded = textInput1.length > 3

    AlertDialog(
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(onClick = onDismiss) {
                Text("Close")
            }
        },
        title = {
            Text(
                text = "Custom Entry",
                style = MaterialTheme.typography.headlineSmall
            )
        },
        text = {
            Column(
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                Box {
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        Text("Category Options")
                        IconButton(onClick = { headerMenuExpanded = true }) {
                            Icon(Icons.Default.ArrowDropDown, contentDescription = null)
                        }
                    }
                    DropdownMenu(
                        expanded = headerMenuExpanded,
                        onDismissRequest = { headerMenuExpanded = false }
                    ) {
                        DropdownMenuItem(
                            text = { Text("Option A") },
                            onClick = { headerMenuExpanded = false }
                        )
                        DropdownMenuItem(
                            text = { Text("Option B") },
                            onClick = { headerMenuExpanded = false }
                        )
                    }
                }

                Box {
                    OutlinedTextField(
                        value = textInput1,
                        onValueChange = { textInput1 = it },
                        label = { Text("Search or Type...") },
                        modifier = Modifier.fillMaxWidth()
                    )
                    DropdownMenu(
                        expanded = textFieldMenuExpanded,
                        onDismissRequest = { },
                        properties = PopupProperties(focusable = false)
                    ) {
                        listOf("Result 1", "Result 2", "Result 3").forEach { suggestion ->
                            DropdownMenuItem(
                                text = { Text(suggestion) },
                                onClick = { textInput1 = suggestion }
                            )
                        }
                    }
                }

                OutlinedTextField(
                    value = textInput2,
                    onValueChange = { textInput2 = it },
                    label = { Text("Additional Notes") },
                    modifier = Modifier.fillMaxWidth()
                )
            }
        }
    )
}

This works but you lose some layout control since AlertDialog has its own structure.


Option 2: Keep Dialog but use ExposedDropdownMenuBox

This is what I actually ended up using. ExposedDropdownMenuBox handles its own anchoring internally so it works fine inside Dialog.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomComposeDialog(onDismiss: () -> Unit) {
    var textInput1 by remember { mutableStateOf("") }
    var textInput2 by remember { mutableStateOf("") }
    var headerMenuExpanded by remember { mutableStateOf(false) }
    var textFieldMenuExpanded by remember { mutableStateOf(false) }

    Dialog(onDismissRequest = onDismiss) {
        Surface(
            shape = RoundedCornerShape(16.dp),
            color = MaterialTheme.colorScheme.surface,
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Column(
                modifier = Modifier.padding(20.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                Text(
                    text = "Custom Entry",
                    style = MaterialTheme.typography.headlineSmall
                )

                // Header dropdown
                ExposedDropdownMenuBox(
                    expanded = headerMenuExpanded,
                    onExpandedChange = { headerMenuExpanded = it }
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .menuAnchor(MenuAnchorType.PrimaryNotEditable)
                            .clickable { headerMenuExpanded = true },
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        Text("Category Options")
                        Icon(Icons.Default.ArrowDropDown, contentDescription = null)
                    }

                    ExposedDropdownMenu(
                        expanded = headerMenuExpanded,
                        onDismissRequest = { headerMenuExpanded = false }
                    ) {
                        DropdownMenuItem(
                            text = { Text("Option A") },
                            onClick = { headerMenuExpanded = false }
                        )
                        DropdownMenuItem(
                            text = { Text("Option B") },
                            onClick = { headerMenuExpanded = false }
                        )
                    }
                }

                // TextField with suggestions
                ExposedDropdownMenuBox(
                    expanded = textFieldMenuExpanded,
                    onExpandedChange = { textFieldMenuExpanded = it }
                ) {
                    OutlinedTextField(
                        value = textInput1,
                        onValueChange = {
                            textInput1 = it
                            textFieldMenuExpanded = it.length > 3
                        },
                        label = { Text("Search or Type...") },
                        modifier = Modifier
                            .fillMaxWidth()
                            .menuAnchor(MenuAnchorType.PrimaryEditable)
                    )

                    if (textInput1.length > 3) {
                        ExposedDropdownMenu(
                            expanded = textFieldMenuExpanded,
                            onDismissRequest = { textFieldMenuExpanded = false }
                        ) {
                            listOf("Result 1", "Result 2", "Result 3").forEach { suggestion ->
                                DropdownMenuItem(
                                    text = { Text(suggestion) },
                                    onClick = {
                                        textInput1 = suggestion
                                        textFieldMenuExpanded = false
                                    }
                                )
                            }
                        }
                    }
                }

                OutlinedTextField(
                    value = textInput2,
                    onValueChange = { textInput2 = it },
                    label = { Text("Additional Notes") },
                    modifier = Modifier.fillMaxWidth()
                )

                TextButton(
                    onClick = onDismiss,
                    modifier = Modifier.align(Alignment.End)
                ) {
                    Text("Close")
                }
            }
        }
    }
}

The important bits:

  • ExposedDropdownMenuBox manages anchoring with the menuAnchor() modifier so it works even inside a Dialog window

  • Use MenuAnchorType.PrimaryNotEditable for the header row since it's not a text field

  • Use MenuAnchorType.PrimaryEditable for the text field so user can keep typing while menu is open

  • Control expanded state in onValueChange based on your length > 3 logic

User AvatarAlpesh Savaliya
Mar 9 at 6:46 PM
1