# DropdownMenu

A click-triggered menu with keyboard navigation, grouped items, checkable options, and nested submenus.

```slint
import { Button } from "@glint/components/button.slint";
import { DropdownMenu } from "@glint/components/dropdown-menu.slint";
import { MenuEntry } from "@glint/components/menu-entry.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    in-out property <string> last-action: "None";

    VerticalLayout {
        alignment: center;
        spacing: 16px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            menu := DropdownMenu {
                items: [
                    { label: "New File", icon: IconSet.FilePlus, shortcut: "Ctrl+N" },
                    { label: "Open…", icon: IconSet.FolderOpen, shortcut: "Ctrl+O" },
                    { label: "Save", icon: IconSet.Save, shortcut: "Ctrl+S" },
                    {
                        label: "Preferences",
                        icon: IconSet.Settings,
                        separator-before: true,
                        children: [
                            { label: "Keyboard Shortcuts", shortcut: "Ctrl+K" },
                            { label: "Settings", shortcut: "Ctrl+," },
                        ]
                    },
                ];
                selected(row, child) => {
                    if (child >= 0) {
                        root.last-action = "Selected submenu item " + child + " in row " + row;
                    } else {
                        root.last-action = "Selected row " + row;
                    }
                }

                Button {
                    text: "File";
                    haspopup: true;
                    clicked => { menu.show(); }
                }
            }
        }

        Text {
            text: "Last action: " + root.last-action;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
    }
}
```

## Usage

```slint
import { Button } from "@glint/components/button.slint";
import { DropdownMenu } from "@glint/components/dropdown-menu.slint";

export component AppWindow inherits Window {
    VerticalLayout {
        alignment: center;

        menu := DropdownMenu {
            items: [
                { label: "Profile" },
                { label: "Billing" },
                { label: "Sign out", separator-before: true }
            ];
            selected(row, child) => {
                // handle choice
            }

            Button {
                text: "My Account";
                haspopup: true;
                clicked => { menu.show(); }
            }
        }
    }
}
```

`DropdownMenu` wraps a trigger element in its `@children` slot (typically a `Button`). Clicking the trigger opens a popup card powered by [MenuPanel](/docs/components/menu-panel) presenting the `items` model. The menu manages keyboard navigation, submenus, checkable items, and radio groups.

The model is two-way: activating a checkbox or radio row immediately updates the `checked` property in the `items` array before the `selected(row, child)` callback fires.

## Examples

### Submenu placement and alignment

When a menu row carries `children`, hovering it or pressing `→` opens a submenu panel. Use `submenu-side` (`PanelSide.right`, `PanelSide.left`) and `submenu-align` (`PanelAlign.start`, `PanelAlign.center`, `PanelAlign.end`) to control where the nested panel appears relative to the parent item.

```slint
import { Button } from "@glint/components/button.slint";
import { DropdownMenu } from "@glint/components/dropdown-menu.slint";
import { PanelSide, PanelAlign } from "@glint/components/panel.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            menu := DropdownMenu {
                submenu-side: PanelSide.left;
                submenu-align: PanelAlign.start;
                min-content-width: 220px;
                items: [
                    { label: "Cut", icon: IconSet.Scissors, shortcut: "Ctrl+X" },
                    { label: "Copy", icon: IconSet.Copy, shortcut: "Ctrl+C" },
                    { label: "Paste", icon: IconSet.Clipboard, shortcut: "Ctrl+V" },
                    {
                        label: "More Options",
                        separator-before: true,
                        children: [
                            { label: "Find and Replace" },
                            { label: "Format Document" },
                        ]
                    }
                ];

                Button {
                    text: "Edit (Opens Left)";
                    haspopup: true;
                    clicked => { menu.show(); }
                }
            }
        }
    }
}
```

### Checkable and radio rows

Set `checkable: true` on an item for a toggleable checkbox row. Adding a non-empty `radio-group` string links the item into a mutually exclusive group where picking one item clears other items in the same group on the same frame.

```slint
import { Button } from "@glint/components/button.slint";
import { DropdownMenu } from "@glint/components/dropdown-menu.slint";
import { MenuEntry } from "@glint/components/menu-entry.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    in-out property <[MenuEntry]> view-items: [
        { label: "Show Bookmarks", checkable: true, checked: true },
        { label: "Show Full URLs", checkable: true, checked: false },
        { label: "Compact Density", separator-before: true, heading-before: "Density", checkable: true, checked: true, radio-group: "density" },
        { label: "Comfortable Density", checkable: true, checked: false, radio-group: "density" },
    ];

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            menu := DropdownMenu {
                items <=> root.view-items;

                Button {
                    text: "View Options";
                    haspopup: true;
                    clicked => { menu.show(); }
                }
            }
        }
    }
}
```

### Group headings and destructive items

Following the [grouped rows](/docs/components/select#grouped-rows) model, use `separator-before: true` and `heading-before: "..."` to group related options without changing item indexing. Set `tone: MenuTone.destructive` on actions that delete or destroy data.

```slint
import { Button } from "@glint/components/button.slint";
import { DropdownMenu } from "@glint/components/dropdown-menu.slint";
import { MenuTone } from "@glint/components/menu-entry.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            menu := DropdownMenu {
                items: [
                    { label: "Edit Item", icon: IconSet.Pencil },
                    { label: "Duplicate", icon: IconSet.Copy },
                    { label: "Archive", separator-before: true, heading-before: "Danger Zone", icon: IconSet.Archive },
                    { label: "Delete", icon: IconSet.Trash2, tone: MenuTone.destructive },
                ];

                Button {
                    text: "Manage Item";
                    haspopup: true;
                    clicked => { menu.show(); }
                }
            }
        }
    }
}
```

## API Reference

### Properties

| Property            | Type                 | Default            | Description                                                                                                                                                                         |
| ------------------- | -------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items`             | `in-out [MenuEntry]` | no default         | Menu rows shown when the trigger is clicked. `in-out` because activating a checkbox or radio row writes its new `checked` back here.                                                |
| `min-content-width` | `in length`          | `200px`            | The floor under the popup panel's width; the panel grows past it to fit its widest row, icon and shortcut hint included.                                                            |
| `submenu-side`      | `in PanelSide`       | `PanelSide.right`  | Where a row's submenu panel opens: which side of the menu, and how it lines up against the row it hangs off. A submenu opens beside its row, so `top` and `bottom` read as `right`. |
| `submenu-align`     | `in PanelAlign`      | `PanelAlign.start` |                                                                                                                                                                                     |
| `highlighted-index` | `in-out int`         | `0`                | Row highlighted by ↑/↓ inside the open popup; consumers rarely set this.                                                                                                            |
| `is-open`           | `out bool`           | no default         | True while the menu is on screen; mirrors `PopupWindow.is-open`.                                                                                                                    |

### Callbacks

| Callback              | Description                                                                                                                                    |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `selected(int , int)` | Fired with the chosen row's index, and with the index of the submenu leaf that was taken or -1 when the row itself was; the popup auto-closes. |

### Functions

| Function  | Description                                                                                                                                                                        |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `show()`  | Opens the menu with the highlight back at the top. Mirrors `PopupWindow.show()` — wire it to a consumer-owned trigger so the menu opens from the keyboard as well as from a click. |
| `close()` | Closes the menu. Mirrors `PopupWindow.close()`.                                                                                                                                    |

### Enums

| Enum         | Values                           |
| ------------ | -------------------------------- |
| `PanelSide`  | `top`, `right`, `bottom`, `left` |
| `PanelAlign` | `start`, `center`, `end`         |

## Accessibility

- **Role and structure.** The open dropdown menu presents an accessible `list` containing `list-item` rows with `accessible-item-count` and 1-based `accessible-item-index` values.
- **Checkable and expandable states.** Checkbox and radio rows announce their `checked` state, and rows with submenus announce themselves as `expandable`.
- **Shortcut hints.** Shortcut strings ride as `accessible-description` on the row rather than creating extra text nodes.
- **Group headings.** Group headers and separator lines are presented as non-actionable labels and never receive keyboard focus.
- **Keyboard navigation.** `↑`/`↓` navigate the list, `Home`/`End` jump to the first and last options, `→` opens a submenu, `←` closes the active submenu, `Enter` or `Space` activates the highlighted option, and `Escape` closes the menu (or the open submenu first).
