---
title: Dropdown Menu
description: An anchored menu component with keyboard navigation and custom placement.
---



```kotlin expandable title="DropdownMenuDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/dropdownmenu/DropdownMenuDemo.kt"
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.ChevronDown
import com.composables.icons.lucide.Clipboard
import com.composables.icons.lucide.Copy
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Maximize
import com.composables.icons.lucide.Scissors
import com.composables.icons.lucide.Trash2
import com.composeunstyled.DropdownMenuPanel
import com.composeunstyled.LocalContentColor
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDropdownMenu
import com.composeunstyled.UnstyledDropdownMenuItem
import com.composeunstyled.UnstyledHorizontalSeparator
import com.composeunstyled.UnstyledIcon
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.errorToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme

@Preview
@Composable
fun DropdownMenuDemo() {
  class DropdownOption(
    val text: String,
    val icon: ImageVector,
    val enabled: Boolean = true,
    val dangerous: Boolean = false,
  )

  val options = listOf(
    DropdownOption("Select All", Lucide.Maximize),
    DropdownOption("Copy", Lucide.Copy),
    DropdownOption("Cut", Lucide.Scissors, enabled = false),
    DropdownOption("Paste", Lucide.Clipboard),
    DropdownOption("Delete", Lucide.Trash2, dangerous = true),
  )
  var expanded by remember { mutableStateOf(true) }

  Box(
    modifier = Modifier.fillMaxSize().padding(top = 24.dp),
    contentAlignment = Alignment.TopCenter,
  ) {
    UnstyledDropdownMenu(
      expanded = expanded,
      onExpandedChange = { expanded = it },
      sideOffset = 4.dp,
      panel = {
        DropdownMenuPanel(
          modifier = Modifier
            .width(240.dp)
            .clip(RectangleShape)
            .background(Theme[colors][surfaceToken])
            .border(1.dp, Theme[colors][borderToken], RectangleShape),
          enter = scaleIn(
            animationSpec = tween(durationMillis = 120, easing = LinearOutSlowInEasing),
            initialScale = 0.8f,
            transformOrigin = TransformOrigin(0f, 0f),
          ) + fadeIn(tween(durationMillis = 30)),
          exit = scaleOut(
            animationSpec = tween(durationMillis = 75),
            targetScale = 0.8f,
            transformOrigin = TransformOrigin(0f, 0f),
          ) +
            fadeOut(tween(durationMillis = 75)),
        ) {
          options.forEachIndexed { index, option ->
            if (index == 1 || index == options.lastIndex) {
              UnstyledHorizontalSeparator(color = Theme[colors][borderToken])
            }
            UnstyledDropdownMenuItem(
              onClick = {},
              enabled = option.enabled,
              indication = LocalIndication.current,
              modifier = Modifier
                .padding(4.dp)
                .sizeIn(minWidth = 40.dp, minHeight = 40.dp)
                .clip(RectangleShape)
                .fillMaxWidth(),
            ) {
              Row(
                modifier = Modifier
                  .fillMaxWidth()
                  .padding(horizontal = 8.dp, vertical = 8.dp),
                horizontalArrangement = Arrangement.Start,
                verticalAlignment = Alignment.CenterVertically,
              ) {
                val contentColor = (
                  if (option.dangerous) {
                    Theme[colors][errorToken]
                  } else {
                    LocalContentColor.current
                  }
                  ).copy(alpha = if (option.enabled) 1f else 0.5f)

                UnstyledIcon(
                  imageVector = option.icon,
                  contentDescription = null,
                  tint = contentColor,
                )
                Spacer(Modifier.width(12.dp))
                Text(
                  text = option.text,
                  color = contentColor,
                )
              }
            }
          }
        }
      },
      anchor = {
        UnstyledButton(
          onClick = { expanded = true },
          modifier = Modifier
            .sizeIn(minWidth = 40.dp, minHeight = 40.dp)
            .clip(RectangleShape)
            .background(Theme[colors][surfaceToken])
            .border(1.dp, Theme[colors][borderToken], RectangleShape),
          indication = LocalIndication.current,
        ) {
          Row(
            modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
            verticalAlignment = Alignment.CenterVertically,
          ) {
            Text("Options")
            Spacer(Modifier.width(8.dp))
            UnstyledIcon(
              imageVector = Lucide.ChevronDown,
              contentDescription = null,
              tint = Theme[colors][contentToken],
            )
          }
        }
      },
    )
  }
}
```



## Features

- Anchor-based placement
- Keyboard menu navigation
- Auto-dismiss on outside click
- Panel enter and exit transitions

## Installation

```kotlin
implementation("com.composables:composeunstyled-dropdown-menu:2.10.0")
```

## Anatomy

```kotlin
UnstyledDropdownMenu(
  expanded = expanded,
  onExpandedChange = onExpandedChange,
  panel = {
    DropdownMenuPanel {
      UnstyledDropdownMenuItem(onClick = onClick) {
      }
    }
  },
  anchor = {
  },
)
```

## Concepts

- `UnstyledDropdownMenu` marks the anchor area and renders the floating menu when expanded.
  - The `anchor` slot renders the content the menu is positioned against.
  - The `panel` slot renders the floating menu content.
- `DropdownMenuPanel` renders the menu surface and arranges direct children vertically.
- `UnstyledDropdownMenuItem` renders a focusable item inside `DropdownMenuPanel`. Use direct `UnstyledDropdownMenuItem` children for managed keyboard navigation. Custom nested layouts can be rendered inside the panel, but nested items are not part of the panel-managed menu order.

## Accessibility

Dropdown menu handles keyboard interactions out of the box. Pressing Arrow Down opens the menu and
focuses the first item. Pressing Arrow Down and Arrow Up moves focus to the next and previous item.
Home moves focus to the first item. End moves focus to the last item. Escape closes the menu. Use
`UnstyledDropdownMenuItem` for focusable menu actions.

## Code Examples

### Opening and closing a dropdown menu

Use the `expanded` parameter to show the menu and the `onExpandedChange` callback to update that state:

```kotlin expandable
var expanded by remember { mutableStateOf(false) }

UnstyledDropdownMenu(
  expanded = expanded,
  onExpandedChange = { expanded = it },
  panel = {
    DropdownMenuPanel {
      UnstyledDropdownMenuItem(onClick = { expanded = false }) {
        BasicText("Close")
      }
    }
  },
  anchor = {
    BasicText(
      text = "Open menu",
      modifier = Modifier.clickable { expanded = true },
    )
  },
)
```

### Positioning a dropdown menu

Use the `side`, `alignment`, `sideOffset`, and `alignmentOffset` parameters to place the menu panel relative to the anchor:

```kotlin expandable
UnstyledDropdownMenu(
  expanded = expanded,
  onExpandedChange = { expanded = it },
  side = AnchorSide.Bottom,
  alignment = AnchorAlignment.End,
  sideOffset = 8.dp,
  panel = {
    DropdownMenuPanel {
      UnstyledDropdownMenuItem(onClick = { select() }) {
        BasicText("Item")
      }
    }
  },
  anchor = {
    BasicText("Open menu")
  },
)
```

### Closing the menu after clicking an item

`UnstyledDropdownMenuItem` closes the menu after click by default by calling the dropdown's `onExpandedChange`
callback with `false`:

```kotlin expandable
UnstyledDropdownMenu(
  expanded = expanded,
  onExpandedChange = { expanded = it },
  panel = {
    DropdownMenuPanel {
      UnstyledDropdownMenuItem(onClick = { select() }) {
        BasicText("Item")
      }
    }
  },
  anchor = {
    BasicText("Open menu")
  },
)
```

### Keeping the menu open after clicking an item

Use the `closeOnClick` parameter when a menu item should update state without dismissing the menu:

```kotlin expandable
UnstyledDropdownMenu(
  expanded = expanded,
  onExpandedChange = { expanded = it },
  panel = {
    DropdownMenuPanel {
      UnstyledDropdownMenuItem(
        closeOnClick = false,
        onClick = { enabled = enabled.not() },
      ) {
        BasicText("Toggle option")
      }
    }
  },
  anchor = {
    BasicText("Open menu")
  },
)
```

### Animating the dropdown menu

Use the `enter` and `exit` parameters on `DropdownMenuPanel` to animate the menu panel:

```kotlin expandable
UnstyledDropdownMenu(
  expanded = expanded,
  onExpandedChange = { expanded = it },
  panel = {
    DropdownMenuPanel(
      enter = fadeIn(),
      exit = fadeOut(),
    ) {
      UnstyledDropdownMenuItem(onClick = { select() }) {
        BasicText("Item")
      }
    }
  },
  anchor = {
    BasicText("Open menu")
  },
)
```

## API Reference

### UnstyledDropdownMenu

| Parameter | Type | Description |
|-----------|------|-------------|
| `expanded` | `Boolean` |  |
| `onExpandedChange` | `(Boolean) -> Unit` |  |
| `modifier` | `Modifier` |  |
| `side` | `AnchorSide` |  |
| `alignment` | `AnchorAlignment` |  |
| `sideOffset` | `Dp` |  |
| `alignmentOffset` | `Dp` |  |
| `panel` | `DropdownMenuScope.() -> Unit` |  |
| `anchor` | `() -> Unit` |  |
### DropdownMenuScope.DropdownMenuPanel

| Parameter | Type | Description |
|-----------|------|-------------|
| `modifier` | `Modifier` |  |
| `enter` | `EnterTransition` |  |
| `exit` | `ExitTransition` |  |
| `content` | `DropdownMenuPanelScope.() -> Unit` |  |
### DropdownMenuPanelScope.MenuItem

| Parameter | Type | Description |
|-----------|------|-------------|
| `onClick` | `() -> Unit` |  |
| `modifier` | `Modifier` |  |
| `enabled` | `Boolean` |  |
| `closeOnClick` | `Boolean` |  |
| `interactionSource` | `MutableInteractionSource?` |  |
| `indication` | `Indication?` |  |
| `content` | `() -> Unit` |  |
