Dropdown Menu
ui-dropdown-menu pairs a ui-dropdown-menu-trigger with a ui-dropdown-menu-content. The content is a native
popover="auto" element with role="menu", anchored to the trigger with CSS anchor positioning, so it closes on
Escape, on a click outside and when focus leaves it. Opening moves focus to the first enabled item, the arrow keys
move between items and Enter or Space chooses one.
The items are shared with Context Menu and Menubar:
ui-menu-item, ui-menu-checkbox-item, ui-menu-radio-group with ui-menu-radio-item, ui-menu-label,
ui-menu-group, ui-menu-separator and ui-menu-shortcut. Choosing an item fires a select event with the item's
value and closes the menu.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dropdown Menu demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dropdown-menu.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/toaster.html"></import-element>
<ui-toaster></ui-toaster>
<ui-dropdown-menu onselect="document.dispatchEvent(new CustomEvent('toast', { detail: { title: 'Selected ' + event.detail.value } }))">
<ui-dropdown-menu-trigger><ui-button variant="outline">Open menu</ui-button></ui-dropdown-menu-trigger>
<ui-dropdown-menu-content>
<ui-menu-label>My Account</ui-menu-label>
<ui-menu-group>
<ui-menu-item value="profile">Profile <ui-menu-shortcut>⇧⌘P</ui-menu-shortcut></ui-menu-item>
<ui-menu-item value="billing">Billing <ui-menu-shortcut>⌘B</ui-menu-shortcut></ui-menu-item>
<ui-menu-item value="settings">Settings <ui-menu-shortcut>⌘S</ui-menu-shortcut></ui-menu-item>
</ui-menu-group>
<ui-menu-separator></ui-menu-separator>
<ui-menu-item value="logout" variant="destructive">Log out <ui-menu-shortcut>⇧⌘Q</ui-menu-shortcut></ui-menu-item>
</ui-dropdown-menu-content>
</ui-dropdown-menu>
</body>
</html>Examples
Handle the selection
select bubbles from the item, so one listener on ui-dropdown-menu handles every item. event.detail.value is the
item's value.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dropdown Menu demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dropdown-menu.html"></import-element>
<import-element from="./dropdown-menu-sort-demo.html"></import-element>
<dropdown-menu-sort-demo></dropdown-menu-sort-demo>
</body>
</html><custom-element name="dropdown-menu-sort-demo">
<script>
const list = useState({ sort: "newest" });
function sorted(event) {
list.sort = event.detail.value;
}
</script>
<style>
:host {
display: flex;
align-items: center;
gap: calc(var(--spacing) * 4);
font-size: var(--text-sm);
}
</style>
<ui-dropdown-menu onselect="sorted(event)">
<ui-dropdown-menu-trigger><ui-button variant="outline">Sort</ui-button></ui-dropdown-menu-trigger>
<ui-dropdown-menu-content>
<ui-menu-item value="newest">Newest first</ui-menu-item>
<ui-menu-item value="oldest">Oldest first</ui-menu-item>
<ui-menu-item value="name">Name</ui-menu-item>
</ui-dropdown-menu-content>
</ui-dropdown-menu>
<span>Sorted by: <slot name="#list.sort"></slot></span>
</custom-element>Checkbox items
ui-menu-checkbox-item toggles its checked attribute when chosen. Its select event carries { value, checked }.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dropdown Menu demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dropdown-menu.html"></import-element>
<ui-dropdown-menu>
<ui-dropdown-menu-trigger><ui-button variant="outline">View</ui-button></ui-dropdown-menu-trigger>
<ui-dropdown-menu-content>
<ui-menu-label>Appearance</ui-menu-label>
<ui-menu-separator></ui-menu-separator>
<ui-menu-checkbox-item value="status-bar" checked>Status Bar</ui-menu-checkbox-item>
<ui-menu-checkbox-item value="activity-bar" disabled>Activity Bar</ui-menu-checkbox-item>
<ui-menu-checkbox-item value="panel">Panel</ui-menu-checkbox-item>
</ui-dropdown-menu-content>
</ui-dropdown-menu>
</body>
</html>Radio items
ui-menu-radio-group holds ui-menu-radio-item elements and keeps one of them checked. Its value attribute is the
checked item's value and updates when another item is chosen.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dropdown Menu demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dropdown-menu.html"></import-element>
<ui-dropdown-menu>
<ui-dropdown-menu-trigger><ui-button variant="outline">Panel position</ui-button></ui-dropdown-menu-trigger>
<ui-dropdown-menu-content>
<ui-menu-label>Panel Position</ui-menu-label>
<ui-menu-separator></ui-menu-separator>
<ui-menu-radio-group value="bottom">
<ui-menu-radio-item value="top">Top</ui-menu-radio-item>
<ui-menu-radio-item value="bottom">Bottom</ui-menu-radio-item>
<ui-menu-radio-item value="right">Right</ui-menu-radio-item>
</ui-menu-radio-group>
</ui-dropdown-menu-content>
</ui-dropdown-menu>
</body>
</html>Icons, disabled and destructive items
A slotted <svg> is sized and muted. disabled items are skipped by the keyboard and ignore clicks.
variant="destructive" colours an item for dangerous actions.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dropdown Menu demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dropdown-menu.html"></import-element>
<ui-dropdown-menu>
<ui-dropdown-menu-trigger><ui-button variant="outline">Actions</ui-button></ui-dropdown-menu-trigger>
<ui-dropdown-menu-content>
<ui-menu-item value="edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" /></svg>
Edit
</ui-menu-item>
<ui-menu-item value="duplicate">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2" /><path d="M4 16V6a2 2 0 0 1 2-2h10" /></svg>
Duplicate
</ui-menu-item>
<ui-menu-item value="share" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 12v8h16v-8M12 3v12M7 8l5-5 5 5" /></svg>
Share
</ui-menu-item>
<ui-menu-separator></ui-menu-separator>
<ui-menu-item value="delete" variant="destructive">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14" /></svg>
Delete
</ui-menu-item>
</ui-dropdown-menu-content>
</ui-dropdown-menu>
</body>
</html>Placement
placement on ui-dropdown-menu-content sets where the menu opens relative to the trigger: bottom-start (default),
bottom-end, top-start or top-end. The menu flips when there is not enough room.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dropdown Menu demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-row-tight {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: calc(var(--spacing) * 2);
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/dropdown-menu.html"></import-element>
<div class="demo-row-tight">
<ui-dropdown-menu>
<ui-dropdown-menu-trigger><ui-button variant="outline">Bottom end</ui-button></ui-dropdown-menu-trigger>
<ui-dropdown-menu-content placement="bottom-end">
<ui-menu-item value="one">First item</ui-menu-item>
<ui-menu-item value="two">Second item</ui-menu-item>
</ui-dropdown-menu-content>
</ui-dropdown-menu>
<ui-dropdown-menu>
<ui-dropdown-menu-trigger><ui-button variant="outline">Top start</ui-button></ui-dropdown-menu-trigger>
<ui-dropdown-menu-content placement="top-start">
<ui-menu-item value="one">First item</ui-menu-item>
<ui-menu-item value="two">Second item</ui-menu-item>
</ui-dropdown-menu-content>
</ui-dropdown-menu>
</div>
</body>
</html>API
Parts
| Part | Slot | Renders |
|---|---|---|
ui-dropdown-menu |
Its parts, with no box of its own. | |
ui-dropdown-menu-trigger |
(default) | An inline block around the trigger. A click on it focuses the element inside and toggles the menu. |
ui-dropdown-menu-content |
(default) | A bordered popover="auto" panel with role="menu", anchored to the trigger. |
ui-menu-item |
(default) | A row with role="menuitem": an optional leading <svg>, the label and an optional shortcut. |
ui-menu-checkbox-item |
(default) | A row with role="menuitemcheckbox" and a check mark when checked. |
ui-menu-radio-group |
(default) | A group (role="group") that keeps one radio item checked. |
ui-menu-radio-item |
(default) | A row with role="menuitemradio" and a dot when checked. Place it in ui-menu-radio-group. |
ui-menu-label |
(default) | A heading row in var(--text-sm) medium. Not focusable. |
ui-menu-group |
(default) | A block with role="group" around related items. |
ui-menu-separator |
(default) | A one-pixel line with role="separator". |
ui-menu-shortcut |
(default) | Muted var(--text-xs) text pushed to the end of its item. Place it in an item. |
The ui-menu-* parts work the same in ui-context-menu-content and ui-menubar-content.
ui-dropdown-menu-content attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
placement |
"bottom-start" | "bottom-end" | "top-start" | "top-end" |
"bottom-start" |
Where the menu opens relative to the trigger. |
ui-menu-item attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value |
string | Sent as event.detail.value when the item is chosen. |
|
disabled |
boolean | false |
Ignores clicks and keys, sets aria-disabled and skips the item in keyboard navigation. |
variant |
"default" | "destructive" |
"default" |
Destructive items use the destructive colour. |
ui-menu-checkbox-item attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value |
string | Sent in event.detail.value. |
|
checked |
boolean | false |
Whether the item is checked. Toggled when chosen. Reflects. |
disabled |
boolean | false |
Ignores clicks and keys and skips the item in keyboard navigation. |
ui-menu-radio-group and ui-menu-radio-item attributes
| Element | Attribute | Type | Default | Description |
|---|---|---|---|---|
ui-menu-radio-group |
value |
string | The checked item's value. Updated when an item is chosen. Reflects. | |
ui-menu-radio-item |
value |
string | The item's value. | |
ui-menu-radio-item |
disabled |
boolean | false |
Ignores clicks and keys and skips the item in keyboard navigation. |
Events
| Event | Element | Detail | Description |
|---|---|---|---|
select |
ui-menu-item, ui-menu-radio-item |
{ value } |
Fired when an enabled item is clicked or activated with Enter or Space. Bubbles and is composed, so you can listen on ui-dropdown-menu. The menu closes afterwards. |
select |
ui-menu-checkbox-item |
{ value, checked } |
Same, with the new checked state. |
CSS parts
| Element | Part | Description |
|---|---|---|
ui-dropdown-menu-trigger |
trigger |
The wrapper that listens for clicks. It is the anchor for positioning. |
ui-dropdown-menu-content |
menu |
The focusgroup wrapper around the items. |
ui-menu-checkbox-item, ui-menu-radio-item |
indicator |
The check mark or dot. |
CSS states
| Name | Description |
|---|---|
ui-menu-item:state(attr-disabled) |
Matches disabled items. |
ui-menu-item:state(attr-variant-is-destructive) |
Matches destructive items. |
ui-menu-checkbox-item:state(attr-checked) |
Matches checked checkbox items. Radio items have the same state. |
ui-dropdown-menu-content:popover-open |
Matches while the menu is open. |
Accessibility
- The content has
role="menu". Items haverole="menuitem",menuitemcheckboxormenuitemradiowitharia-checked, andtabindex="-1". Groups and the radio group haverole="group", separatorsrole="separator". - Opening the menu focuses the first enabled item. Arrow Up and Arrow Down move focus and wrap around. Home and End jump to the first and last item.
- Enter and Space choose the focused item. Escape, a click outside or moving focus out of the menu closes it.
- When the menu closes with focus inside it, focus returns to the trigger.