I am trying to implement a custom Jetpack Compose Dialog that contains two elements with their own anchored DropdownMenu:
A Header Row with a downward icon that should trigger a menu when clicked.
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!


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:
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.
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
Alpesh Savaliya