Dropdown button
Dropdown button is a trigger that reveals a short menu of actions on click, opening as a floating panel anchored to the trigger rather than navigating anywhere. Picking a choice fires that action right away and closes the menu, rather than setting a value that stays selected like a traditional dropdown. Menu rows can be plain text, or carry their own icon, description, or a nested submenu of further choices.
Dependencies
When to use
Use a dropdown button when one trigger needs to reveal a short menu of
actions, such as a row's "⋯" menu with Edit, Duplicate, and Delete, or a
"New" button offering a few different item types. If there's really only
one action behind it, that's just a plain
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/dropdown-button.js";
import "@eunomia/elements/button.js";
import "@eunomia/elements/icon.js";
<eun-dropdown-button label="Actions"></eun-dropdown-button>
const dropdown = document.querySelector("eun-dropdown-button");
dropdown.options = [
{ key: "edit", label: "Edit" },
{ key: "duplicate", label: "Duplicate" },
{ key: "delete", label: "Delete" },
];
dropdown.addEventListener("eunselect", (event) => {
console.log(event.key); // "edit", "duplicate", or "delete"
});
Importing the file registers <eun-dropdown-button> as a custom element,
with no further setup needed. It works with any framework, or none, since
it's a standard web component. eun-button and eun-icon are also
rendered internally for the trigger and its option icons, so import both
alongside.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/dropdown-button.js";
import "@eunomia/elements/button.js";
import "@eunomia/elements/icon.js";
</script>
<eun-dropdown-button id="actions" label="Actions"></eun-dropdown-button>
<script type="module">
const dropdown = document.querySelector("#actions");
dropdown.options = [
{ key: "edit", label: "Edit" },
{ key: "duplicate", label: "Duplicate" },
{ key: "delete", label: "Delete" },
];
dropdown.addEventListener("eunselect", (event) => {
console.log(event.key);
});
</script>
npm install @eunomia/elements
import "@eunomia/elements/dropdown-button.js";
import "@eunomia/elements/button.js";
import "@eunomia/elements/icon.js";
const OPTIONS = [
{ key: "edit", label: "Edit" },
{ key: "duplicate", label: "Duplicate" },
{ key: "delete", label: "Delete" },
];
function RowActions({ onAction }) {
return (
<eun-dropdown-button
label="Actions"
ref={(el) => {
if (el) {
el.options = OPTIONS;
}
}}
oneunselect={(event) => onAction(event.key)}
/>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/dropdown-button.js";
import "@eunomia/elements/button.js";
import "@eunomia/elements/icon.js";
const OPTIONS = [
{ key: "edit", label: "Edit" },
{ key: "duplicate", label: "Duplicate" },
{ key: "delete", label: "Delete" },
];
export function RowActions({ onAction }) {
return (
<eun-dropdown-button
label="Actions"
ref={(el) => {
if (el) {
el.options = OPTIONS;
}
}}
oneunselect={(event) => onAction(event.key)}
/>
);
}
npm install @eunomia/elements
<script setup>
import { ref, onMounted } from "vue";
import "@eunomia/elements/dropdown-button.js";
import "@eunomia/elements/button.js";
import "@eunomia/elements/icon.js";
const props = defineProps(["onAction"]);
const dropdown = ref(null);
onMounted(() => {
dropdown.value.options = [
{ key: "edit", label: "Edit" },
{ key: "duplicate", label: "Duplicate" },
{ key: "delete", label: "Delete" },
];
});
</script>
<template>
<eun-dropdown-button
ref="dropdown"
label="Actions"
@eunselect="(event) => props.onAction(event.key)"
/>
</template>
npm install @eunomia/elements
import {
CUSTOM_ELEMENTS_SCHEMA,
Component,
ElementRef,
AfterViewInit,
ViewChild,
Output,
EventEmitter,
} from "@angular/core";
import "@eunomia/elements/dropdown-button.js";
import "@eunomia/elements/button.js";
import "@eunomia/elements/icon.js";
@Component({
selector: "app-row-actions",
template: `<eun-dropdown-button
#dropdown
label="Actions"
(eunselect)="onSelect($event)"
></eun-dropdown-button>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class RowActionsComponent implements AfterViewInit {
@ViewChild("dropdown") dropdown!: ElementRef;
@Output() action = new EventEmitter<string>();
ngAfterViewInit() {
this.dropdown.nativeElement.options = [
{ key: "edit", label: "Edit" },
{ key: "duplicate", label: "Duplicate" },
{ key: "delete", label: "Delete" },
];
}
onSelect(event: CustomEvent & { key: string }) {
this.action.emit(event.key);
}
}
Alternatives
eun-buttoneun-selectGuidance
- Use
eun-dropdown-buttonwhen a single trigger should reveal several possible actions (e.g. a row's "⋮" menu: Edit, Duplicate, Delete) - Set
aria-labelon an icon-only trigger (no visiblelabel/slot content) so it still has an accessible name, forwarded straight to the underlying button (see the Accessibility tab) - Reach for slotted
eun-dropdown-itemas soon as a row needs more than a plain stringlabel:optionsis the compact shape for the common case, not the ceiling - Reserve nested submenus for a genuine second level, whichever mechanism builds them, since each extra level of nesting costs the person navigating it more clicks and keystrokes
- Using it for a single action: that's just a plain
eun-button - Using it to pick a value that should persist as form state: that's what
is foreun-select - Overriding colors with inline styles instead of the
--dropdown-menu-*CSS variables or the trigger's own--button-*variables
Building the menu from slotted items instead of (or alongside) options?
Import @eunomia/elements/dropdown-item.js too, and eun-icon again for an
item's own icon. eun-dropdown-item is documented alongside this page
rather than on a page of its own, see "Custom items" in the Examples tab.
Live testing
Properties
options/disabledKeys are plain JS array properties in normal usage (see
the Examples tab). The JSON-serialized attribute form above only exists so
this playground (which round-trips markup through an HTML string) can set
them too.
Dropdown button <eun-dropdown-button>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| options | Array<EunomiaDropdownOption> | [] | The menu options |
| disabled-keys | string[] | [] | Keys of options to disable in addition to any option already marked disabled |
| aria-label | string | — | The accessible label for the trigger button, required when it has no visible label or slotted content, such as an icon only trigger |
| label | string | — | The trigger button's label |
| icon | EunomiaIconName | — | An optional leading icon for the trigger button. Ignored once something is slotted into start, and overridden by the default icon from severity when both are set |
| severity | 'info' | 'success' | 'warning' | 'critical' | 'neutral' | — | The severity of the action this trigger leads to. When set, it supplies a default leading icon unless icon is also set or something is slotted into start. It has no effect on color, which is still controlled by variant |
| disabled | boolean | false | Whether the trigger button is disabled |
| size | 'xs' | 's' | 'm' | 'l' | 'xl' | 'm' | The size of the trigger button |
| variant | 'primary' | 'secondary' | 'critical' | 'success' | 'warning' | — | The color theme applied to the trigger button |
| rounded | boolean | false | Whether the trigger button has fully rounded corners |
| appearance | 'default' | 'fill' | 'outline' | 'flat' | 'raised' | 'ghost' | 'default' | The trigger button's surface style, using the same values as the button component. Defaults to a paler tint rather than a solid fill, since a menu trigger reads better as a lighter weight control than a standalone primary action |
| placement | EunomiaDropdownPlacement | 'bottom-start' | Where the menu opens relative to the trigger |
| open | boolean | false | Whether the menu is currently open. Read-only in practice (set from the menu's own native toggle handling), reflected as an attribute so a consumer's own CSS can select on it, e.g. to keep a hover-revealed trigger visible while its menu is open |
Import the exact TypeScript type behind any property above, see
Slots
| Name | Description |
|---|---|
| (default) | The trigger's label content, if not using the label property |
| items | Dropdown item elements, used instead of or alongside options |
| start | An icon or other content shown at the start of the trigger. Takes priority over the default icon from icon or severity |
Events
| Name | Type | Description |
|---|---|---|
| eunselect | SelectEvent | Fired when an option is chosen |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --dropdown-menu-background | Sets the background color of the menu |
| --dropdown-menu-border-color | Sets the border color of the menu |
| --dropdown-menu-border-radius | Sets the corner radius of the menu |
| --dropdown-menu-shadow | Sets the box shadow of the menu |
| --dropdown-menu-color | Sets the text color of the options |
| --dropdown-menu-description-color | Sets the color of an option's description and submenu arrow |
| --dropdown-menu-disabled-color | Sets the text color of a disabled option |
| --dropdown-menu-hover-background | Sets the background color of a highlighted option |
| --dropdown-menu-accent-color | Sets the color of the hover accent bar and the keyboard focus outline. Also inherited by slotted dropdown items |
| --button-border-color | Sets the border color of the trigger button |
| --button-border-color-hover | Sets the border color of the trigger button on hover or focus, falling back to button-border-color when unset |
Option shape
Each entry of options is a plain object:
interface EunomiaDropdownOption {
key: string;
label: string;
description?: string;
icon?: string; // a EunomiaIconName, e.g. "edit"
disabled?: boolean;
// Nested options, opened as a flyout submenu — one level deep only.
options?: EunomiaDropdownOption[];
}
Into a menu, with custom content
eun-dropdown-item is the escape hatch for a menu row whose content doesn't
fit options' plain label/icon/description shape (rich markup, or a
submenu nested more than one level deep). It must be placed inside a
eun-dropdown-button's slot="items", or inside another
eun-dropdown-item's slot="submenu" for a nested flyout. It delegates
selection, submenu coordination, and Tab handling to whichever is its
closest ancestor.
Dropdown item <eun-dropdown-item>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| key | string | — | The key reported when this item is chosen |
| label | string | — | The item's text content, if not using the default slot |
| icon | EunomiaIconName | — | An optional leading icon |
| description | string | — | Optional secondary text shown under the label or slotted content |
| disabled | boolean | false | Whether this item is disabled |
| active | boolean | false | Marks this item as the menu's current choice. This is a visual state only and isn't set automatically |
Import the exact TypeScript type behind any property above, see
Slots
| Name | Description |
|---|---|
| (default) | The item's content, if not using the label property. Can hold any markup, not just text |
| icon | Replaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty |
| submenu | Nested dropdown item elements, opened as this item's own submenu |
CSS custom properties
| Name | Description |
|---|---|
| --dropdown-menu-color | Sets the text color of this item |
| --dropdown-menu-description-color | Sets the color of this item's description and submenu arrow |
| --dropdown-menu-disabled-color | Sets the text color of this item when disabled |
| --dropdown-menu-hover-background | Sets the background color of this item when highlighted or when its submenu is open |
| --dropdown-menu-accent-color | Sets the color of the hover accent bar and the keyboard focus outline |
| --dropdown-menu-background | Sets the background color of this item's own submenu panel, if it has one |
| --dropdown-menu-border-color | Sets the border color of this item's own submenu panel, if it has one |
| --dropdown-menu-border-radius | Sets the corner radius of this item's own submenu panel, if it has one |
| --dropdown-menu-shadow | Sets the box shadow of this item's own submenu panel, if it has one |
CSS shadow parts
| Name | Description |
|---|---|
| item | The item's own row (icon, label/description, submenu chevron), carrying its padding and hover/active background. Exposed for effects the item's CSS custom properties don't cover |
Basic
<eun-dropdown-button label="Actions"></eun-dropdown-button>
document.querySelector("eun-dropdown-button").options = [
{ key: "edit", label: "Edit" },
{ key: "duplicate", label: "Duplicate" },
{ key: "delete", label: "Delete" },
];
Custom items
Slot eun-dropdown-item elements into slot="items" instead of (or
alongside) options, whenever a row needs more than plain text: each item
renders its own slotted content, so it isn't limited to a string label.
Basic items
<eun-dropdown-button label="Actions">
<eun-dropdown-item slot="items" key="edit">Edit</eun-dropdown-item>
<eun-dropdown-item slot="items" key="duplicate">Duplicate</eun-dropdown-item>
<eun-dropdown-item slot="items" key="delete">Delete</eun-dropdown-item>
</eun-dropdown-button>
Icon, description, and disabled
icon and description work the same way as an options entry's own
properties. disabled is set directly on the item instead of going through
disabledKeys.
<eun-dropdown-button label="Actions">
<eun-dropdown-item
slot="items"
key="edit"
icon="edit"
label="Edit"
description="Modify this item's content"
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
key="archive"
icon="archive"
label="Archive"
disabled
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
key="delete"
icon="delete"
label="Delete"
description="This action cannot be undone"
></eun-dropdown-item>
</eun-dropdown-button>
Custom icon
Need something other than a eun-icon, or a different icon library
entirely? Slot your own content into icon instead. It replaces the
prop-driven icon entirely.
<eun-dropdown-button label="Actions">
<eun-dropdown-item slot="items" key="edit" label="Edit">
<svg
slot="icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
></path>
<path d="M18.5 2.5a2.1 2.1 0 0 1 3 3L12 15l-4 1 1-4Z"></path>
</svg>
</eun-dropdown-item>
<eun-dropdown-item
slot="items"
key="delete"
label="Delete"
></eun-dropdown-item>
</eun-dropdown-button>
Rich content
This is the actual payoff of slots over options: the default slot can
hold any markup, not just a plain string, as shown here with a trailing eun-tag.
<eun-dropdown-button label="Actions">
<eun-dropdown-item slot="items" key="upgrade">
Upgrade plan
<eun-tag size="s" severity="success">New</eun-tag>
</eun-dropdown-item>
<eun-dropdown-item slot="items" key="delete">Delete</eun-dropdown-item>
</eun-dropdown-button>
Nested submenu items
Nest eun-dropdown-item elements in slot="submenu" for a flyout, opened
on hover, click, or ArrowRight, the same interaction as an options entry's
own nested options, except an item's submenu can itself nest further
submenus, recursively.
<eun-dropdown-button label="Actions">
<eun-dropdown-item
slot="items"
key="edit"
icon="edit"
label="Edit"
></eun-dropdown-item>
<eun-dropdown-item slot="items" key="share" icon="share" label="Share">
<eun-dropdown-item
slot="submenu"
key="share-link"
icon="link"
label="Copy link"
></eun-dropdown-item>
<eun-dropdown-item
slot="submenu"
key="share-email"
icon="mail"
label="Share by email"
></eun-dropdown-item>
</eun-dropdown-item>
<eun-dropdown-item
slot="items"
key="delete"
icon="delete"
label="Delete"
></eun-dropdown-item>
</eun-dropdown-button>
Custom trigger content
start isn't limited to a eun-icon — any markup works, here a plain
colored dot. Combined with label, that's enough to build a trigger
that mirrors the current choice.
<eun-dropdown-button label="Status">
<span slot="start" class="status-dot" style="background: #94a3b8"></span>
<eun-dropdown-item slot="items" key="online">Online</eun-dropdown-item>
<eun-dropdown-item slot="items" key="away">Away</eun-dropdown-item>
<eun-dropdown-item slot="items" key="offline">Offline</eun-dropdown-item>
</eun-dropdown-button>
const STATUSES = {
online: { label: "Online", color: "#22c55e" },
away: { label: "Away", color: "#f59e0b" },
offline: { label: "Offline", color: "#94a3b8" },
};
const trigger = document.querySelector("eun-dropdown-button");
const dot = trigger.querySelector('[slot="start"]');
trigger.addEventListener("eunselect", (event) => {
const status = STATUSES[event.key];
trigger.label = status.label;
dot.style.background = status.color;
});
Neither label nor the slotted start content update on their own when an item is chosen (see "Unlike eun-select..." in the Overview tab) : listen for eunselect and set them yourself, as above.
Decoupled label
Nothing requires the trigger to track the selection at all — leave it
alone and it simply won't, no matter what gets picked. Here label and
the dot both stay put, with no eunselect listener whatsoever.
<eun-dropdown-button label="Status">
<span slot="start" class="status-dot" style="background: #6366f1"></span>
<eun-dropdown-item slot="items" key="online">Online</eun-dropdown-item>
<eun-dropdown-item slot="items" key="away">Away</eun-dropdown-item>
<eun-dropdown-item slot="items" key="offline">Offline</eun-dropdown-item>
</eun-dropdown-button>
Using options
The same menu, built both ways: pick whichever is more convenient for how
the choices are produced in your code. options entries render first,
followed by any slotted items, so the two can be mixed in one menu too.
<!-- options : a data-driven array, set as a JS property -->
<eun-dropdown-button label="Actions" id="options-example"></eun-dropdown-button>
<script>
document.querySelector("#options-example").options = [
{ key: "edit", label: "Edit", icon: "edit" },
{ key: "duplicate", label: "Duplicate", icon: "content_copy" },
{ key: "delete", label: "Delete", icon: "delete" },
];
</script>
<!-- slotted items : declared directly in markup -->
<eun-dropdown-button label="Actions">
<eun-dropdown-item
slot="items"
key="edit"
icon="edit"
label="Edit"
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
key="duplicate"
icon="content_copy"
label="Duplicate"
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
key="delete"
icon="delete"
label="Delete"
></eun-dropdown-item>
</eun-dropdown-button>
With icons
The trigger's leading icon can be set either through the icon property,
or by slotting a eun-icon (or any content) into start, a manual
override for anything the icon prop's plain string can't express. Both
produce the same result for a plain icon, so reach for the slot only once
it needs to be something else.
<eun-dropdown-button
icon="share"
label="Share (icon prop)"
></eun-dropdown-button>
<!-- equivalent, spelled out by hand -->
<eun-dropdown-button label="Share (slot)">
<eun-icon slot="start" name="share"></eun-icon>
</eun-dropdown-button>
Icon only
<eun-dropdown-button
icon="more_vert"
aria-label="More actions"
></eun-dropdown-button>
Without a visible label (or slotted content), the trigger has no accessible name on its own: always set aria-label directly on eun-dropdown-button for an icon-only trigger. It's forwarded to the underlying button for you (see the Accessibility tab).
Disabled
<eun-dropdown-button label="Actions" disabled></eun-dropdown-button>
Disabled options
An option can be disabled individually ({ disabled: true }), or in bulk via disabledKeys, handy when disabling depends on external, per-render state (e.g. permissions) without rebuilding the whole options array.
document.querySelector("eun-dropdown-button").disabledKeys = ["delete"];
Icons and descriptions
Each option can optionally have an icon and a description, rendered
under its label, which centers vertically on its own when there's no
description.
const options = [
{
key: "edit",
label: "Edit",
icon: "edit",
description: "Modify this item's content",
},
{
key: "delete",
label: "Delete",
icon: "delete",
description: "This action cannot be undone",
},
];
Submenus
Nested options open as a flyout submenu on hover, click, or ArrowRight,
one level deep only, so a sub-option's own nested options (if any) are
ignored.
const options = [
{ key: "edit", label: "Edit", icon: "edit" },
{
key: "share",
label: "Share",
icon: "share",
options: [
{ key: "share-link", label: "Copy link", icon: "link" },
{ key: "share-email", label: "Share by email", icon: "mail" },
],
},
{ key: "delete", label: "Delete", icon: "delete" },
];
Sizes
size is forwarded straight to the internal eun-button: xs, s, m
(default), l, or xl, affecting only the trigger, not the menu itself.
<eun-dropdown-button size="xs" label="Actions"></eun-dropdown-button>
<eun-dropdown-button size="s" label="Actions"></eun-dropdown-button>
<eun-dropdown-button size="m" label="Actions"></eun-dropdown-button>
<eun-dropdown-button size="l" label="Actions"></eun-dropdown-button>
<eun-dropdown-button size="xl" label="Actions"></eun-dropdown-button>
Part of the shared vocabulary covered in
Trigger styles
appearance is forwarded straight to the internal eun-button: every
button visual prop applies identically (see the
Button page for the full list). Defaults to
default here rather than eun-button's own fill default, since a menu
trigger reads better as a lighter-weight control than a standalone
primary action button.
ghost has no background or border at rest by design, so hover or focus
it above to see it appear. The dashed outline shown here is added for
this demo only, so its hit area stays visible at rest, and isn't part of
the real component (see the plain markup below).
<eun-dropdown-button label="Fill" appearance="fill"></eun-dropdown-button>
<eun-dropdown-button label="Default" appearance="default"></eun-dropdown-button>
<eun-dropdown-button label="Outline" appearance="outline"></eun-dropdown-button>
<eun-dropdown-button label="Raised" appearance="raised"></eun-dropdown-button>
<eun-dropdown-button label="Flat" appearance="flat"></eun-dropdown-button>
<eun-dropdown-button label="Ghost" appearance="ghost"></eun-dropdown-button>
Variants
variant is forwarded straight to the internal eun-button: primary
(default), secondary, critical, success, or warning. See the
Button page's own "Variants" section for what each
color means.
<eun-dropdown-button label="Primary"></eun-dropdown-button>
<eun-dropdown-button
label="Secondary"
variant="secondary"
></eun-dropdown-button>
<eun-dropdown-button label="Critical" variant="critical"></eun-dropdown-button>
<eun-dropdown-button label="Success" variant="success"></eun-dropdown-button>
<eun-dropdown-button label="Warning" variant="warning"></eun-dropdown-button>
Severity
severity (info, success, warning, critical, or neutral, the
same vocabulary as Alert/Tag)
sets the trigger's leading icon by default, once set and icon isn't. It
has no effect on color, which stays driven by variant: a full severity
range doesn't have a matching action color (no "info"/"neutral" button),
so the two stay independent rather than one implying the other.
<eun-dropdown-button label="Info" severity="info"></eun-dropdown-button>
<eun-dropdown-button label="Success" severity="success"></eun-dropdown-button>
<eun-dropdown-button label="Warning" severity="warning"></eun-dropdown-button>
<eun-dropdown-button label="Critical" severity="critical"></eun-dropdown-button>
<eun-dropdown-button label="Neutral" severity="neutral"></eun-dropdown-button>
An explicit icon always takes precedence over severity's own default: set one to override it.
Placement
placement controls where the menu opens relative to the trigger:
top-start, top, top-end, right, bottom-end, bottom,
bottom-start (default), or left. It flips to the opposite side on its
own when there isn't enough viewport room on the requested one, easiest
to see by placing this page's scrollbar near an edge and reopening a
menu.
<eun-dropdown-button placement="top-end" label="Actions"></eun-dropdown-button>
Custom menu colors
Override the --dropdown-menu-* CSS variables: full list in the API tab.
They're inherited through the DOM tree, including across the slot="items"
boundary into a slotted eun-dropdown-item's own shadow root, so setting
them once on eun-dropdown-button itself restyles options rows and
slotted items alike, consistently.
<eun-dropdown-button class="custom-dropdown" label="Actions">
<eun-dropdown-item
slot="items"
key="share"
icon="share"
label="Share"
></eun-dropdown-item>
</eun-dropdown-button>
.custom-dropdown {
--dropdown-menu-background: #fdf2f8;
--dropdown-menu-border-color: #fbcfe8;
--dropdown-menu-color: #831843;
--dropdown-menu-description-color: #be185d;
--dropdown-menu-disabled-color: #f0abfc;
--dropdown-menu-hover-background: #fce7f3;
--dropdown-menu-accent-color: #db2777;
}
Keyboard interactions
| Key | Action |
|---|---|
Enter / Space on the trigger |
Opens the menu and focuses its first entry |
ArrowDown / ArrowUp |
Moves focus between entries (options, then any slotted items), skipping disabled ones entirely (they're never a valid focus target: not via arrow navigation, Home/End, or a mouse click) |
Home / End |
Jumps to the first/last entry |
Enter / Space on an entry |
Selects it, or opens its submenu if it has one |
ArrowRight on an entry with a submenu |
Opens it and focuses its first item |
ArrowLeft / Escape within a submenu |
Closes it and returns focus to its parent entry |
Escape |
Closes the menu (native Popover API light-dismiss), returning focus to the trigger |
Tab / Shift+Tab |
Closes the whole menu (every open submenu at every level, not just the current one) and lets focus move on to wherever it naturally would next. Native Popover light-dismiss doesn't handle this on its own (only outside clicks, Escape, or opening another popover do), so it's handled explicitly |
Aria attributes and rules
Follows the WAI-ARIA menu button pattern, identically whether an entry comes
from options or is a slotted eun-dropdown-item:
aria-haspopup="menu"andaria-expandedon the trigger.role="menu"on the panel,role="menuitem"on each entry: real roving DOM focus (each enabled entry is individually focusable), not simulated viaaria-activedescendant. Aeun-dropdown-itemsetsrole="menuitem"on itself, not on some internal element.- An entry with a submenu additionally gets
aria-haspopup="menu"andaria-expanded, mirroring the trigger's own pattern one level down, recursively, for however deep aeun-dropdown-item's own nested submenus go. - Disabled entries carry
aria-disabled="true"and are excluded from the tab/arrow-key order entirely, rather than left focusable-but-inert. - Every entry shows a visible focus indicator (inset outline) when reached via keyboard, distinct from but as prominent as the hover/highlighted state.
- Only one submenu is ever open per branch at a time: opening a sibling's
submenu closes whichever was open, whether the two are
optionsentries,eun-dropdown-items, or a mix of both.
Label accessibility
The trigger has no accessible name of its own beyond its visible
label/slot content: set aria-label directly on eun-dropdown-button
for an icon-only trigger (see "Icon only" in the Examples tab). It's
forwarded straight to the underlying eun-button.
Reference links