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
| You want… | Prefers |
|---|---|
| Only ever one action behind the trigger, not a menu of several | eun-button |
| Picking an option to set a value that persists as form state, rather than firing an action | eun-select |
| Links to other pages in a site's navigation, rather than actions | eun-nav-menu |
Guidance
- 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 |
| fixed-color | boolean | false | Keeps a light or dark trigger button white or black in every theme. Without it, they follow the grey scale, which flips with the active theme. Has no effect on the other colors |
| 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 color |
| disabled | boolean | false | Whether the trigger button is disabled |
| size | 'xs' | 's' | 'm' | 'l' | 'xl' | 'm' | The size of the trigger button |
| color | 'primary' | 'secondary' | 'critical' | 'success' | 'warning' | — | The color applied to the trigger button |
| rounded | boolean | false | Whether the trigger button has fully rounded corners |
| full | boolean | false | Whether the trigger stretches to fill 100% of the width available from its parent, instead of sizing to its content. A width set on the element itself, or --button-width, stretches it too |
| appearance | 'soft' | 'fill' | 'outline' | 'flat' | 'raised' | 'ghost' | 'fill' | The trigger button's surface style, using the same values as the button component |
| placement | EunomiaDropdownPlacement | 'bottom-start' | Where the menu opens relative to the trigger |
| selectable | boolean | false | Turns the menu into a single choice, keeping the chosen option's key as value and marking it in the menu. Without it, the menu is a menu of actions and value is ignored |
| multiple | boolean | false | Lets several options be checked at once, while selectable is set. Choosing an option toggles it and keeps the menu open, and value becomes the list of every checked key |
| value | string | string[] | — | The key of the current choice, while selectable is set, or every checked key while multiple. Reflected as an attribute, a comma-separated list while multiple and only while a choice is set, so a menu with a choice set can be styled with eun-dropdown-button[value] |
| clearable | boolean | false | Lets choosing the current option again clear value, while selectable is set. Has no effect while multiple, where choosing a checked option unchecks it |
| 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 |
| eunchange | ChangeEvent | Fired when value changes, while selectable is set. Carries the new value, undefined once cleared, or every checked key while multiple |
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 |
| --dropdown-menu-focus-outline-color | Sets the keyboard focus outline color of an option, falling back to --dropdown-menu-accent-color. Also inherited by slotted dropdown items |
| --button-border-color | Sets the border color of the trigger button |
| --button-width | Sets the trigger's width, such as 100% to stretch it like the full attribute does |
| --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 |
|---|---|---|---|
| value | string | — | The key reported when this item is chosen |
| key | string | — | Alias of value, kept for compatibility. Prefer value, since React and Vue reserve key and never pass it to the element |
| 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. A visual state set by hand, unless the dropdown button is selectable, which then sets it from its own value |
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-focus-outline-color | Sets the keyboard focus outline color, falling back to --dropdown-menu-accent-color |
| --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.
Each item's value is the key eunselect reports when it's chosen. The
older key attribute still works as an alias, but React and Vue reserve
key and never pass it to the element, so use value there.
Basic items
<eun-dropdown-button label="Actions">
<eun-dropdown-item slot="items" value="edit">Edit</eun-dropdown-item>
<eun-dropdown-item slot="items" value="duplicate"
>Duplicate</eun-dropdown-item
>
<eun-dropdown-item slot="items" value="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"
value="edit"
icon="edit"
label="Edit"
description="Modify this item's content"
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
value="archive"
icon="archive"
label="Archive"
disabled
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
value="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" value="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"
value="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" value="upgrade">
Upgrade plan
<eun-tag size="s" severity="success">New</eun-tag>
</eun-dropdown-item>
<eun-dropdown-item slot="items" value="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"
value="edit"
icon="edit"
label="Edit"
></eun-dropdown-item>
<eun-dropdown-item slot="items" value="share" icon="share" label="Share">
<eun-dropdown-item
slot="submenu"
value="share-link"
icon="link"
label="Copy link"
></eun-dropdown-item>
<eun-dropdown-item
slot="submenu"
value="share-email"
icon="mail"
label="Share by email"
></eun-dropdown-item>
</eun-dropdown-item>
<eun-dropdown-item
slot="items"
value="delete"
icon="delete"
label="Delete"
></eun-dropdown-item>
</eun-dropdown-button>
Custom trigger content
start isn't limited to a eun-icon, since 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" value="online">Online</eun-dropdown-item>
<eun-dropdown-item slot="items" value="away">Away</eun-dropdown-item>
<eun-dropdown-item slot="items" value="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" value="online">Online</eun-dropdown-item>
<eun-dropdown-item slot="items" value="away">Away</eun-dropdown-item>
<eun-dropdown-item slot="items" value="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"
value="edit"
icon="edit"
label="Edit"
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
value="duplicate"
icon="content_copy"
label="Duplicate"
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
value="delete"
icon="delete"
label="Delete"
></eun-dropdown-item>
</eun-dropdown-button>
Menu content
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",
},
];
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"];
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" },
];
Selection
Single choice
For a quick sort or filter in a toolbar, selectable turns the menu into
a single choice. The chosen option stays marked in the menu, its key is
kept as value, and eunchange fires with the new value. With
clearable, choosing the current option again removes it. value is
reflected as an attribute, so the trigger can show that a choice is set,
here by switching to a soft primary button.
<eun-dropdown-button label="Sort" selectable clearable color="light">
<eun-dropdown-item slot="items" value="recent">Most recent</eun-dropdown-item>
<eun-dropdown-item slot="items" value="name">Name</eun-dropdown-item>
<eun-dropdown-item slot="items" value="price">Price</eun-dropdown-item>
</eun-dropdown-button>
/* Restyle the trigger while a choice is set */
eun-dropdown-button[value] {
--button-border-color: var(--eun-color-primary-500);
}
Multiple choice
For a filter that combines several options, add multiple to
selectable. Each option then toggles on its own and the menu stays open
between choices, until Escape, a click outside or Tab closes it.
value lists every checked key, and eunchange fires with that list.
clearable has no effect here, since choosing a checked option unchecks
it. In markup, value is a comma-separated list, and the attribute is
removed once nothing is checked, so eun-dropdown-button[value] still
tells a filter that's applied.
<eun-dropdown-button label="Categories" selectable multiple value="taps">
<eun-dropdown-item slot="items" value="taps" label="Taps"></eun-dropdown-item>
<eun-dropdown-item
slot="items"
value="showers"
label="Showers"
></eun-dropdown-item>
<eun-dropdown-item
slot="items"
value="valves"
label="Valves"
></eun-dropdown-item>
</eun-dropdown-button>
filter.addEventListener("eunchange", (event) => {
console.log(event.value); // ["taps", "showers"]
});
Trigger
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).
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), with the same
fill default.
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="Soft" appearance="soft"></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>
Colors
color is forwarded straight to the internal eun-button: primary
(default), secondary, critical, success, or warning, plus the neutral
light, medium and dark, and fixed-color for the last two. See the
Button page's own "Colors" section for what each
color means.
<eun-dropdown-button label="Primary"></eun-dropdown-button>
<eun-dropdown-button label="Secondary" color="secondary"></eun-dropdown-button>
<eun-dropdown-button label="Critical" color="critical"></eun-dropdown-button>
<eun-dropdown-button label="Success" color="success"></eun-dropdown-button>
<eun-dropdown-button label="Warning" color="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 color: 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.
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
Full width
full stretches the trigger to the full width of its parent, like on
the Button, and the menu opens at least as wide
as the trigger. To stretch it only below a breakpoint, set --button-width
to 100% on it from a @container or @media rule instead, as on the
button.
<eun-dropdown-button full label="Sort"></eun-dropdown-button>
@media (width < 768px) {
.toolbar eun-dropdown-button {
--button-width: 100%;
}
}
Disabled
<eun-dropdown-button label="Actions" disabled></eun-dropdown-button>
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>
Server-side rendering
popover="auto" surface), which doesn't exist yet in
server-rendered HTML. Items slotted into items render
as a plain, unstyled list stacked below the trigger first, then
disappear the instant the element upgrades and the popover takes
over. The trigger's own label is unaffected, since it's never
hidden to begin with.
Link overlay-ssr.css in the page's <head> to fix this, alongside every
other closed-by-default overlay component it covers in one file (see its
own usage comment for the full list):
<link
rel="stylesheet"
href="node_modules/@eunomia/elements/dist/ssr/overlay-ssr.css"
/>
A bundler that resolves package exports (Vite, webpack, Turbopack, or a
Sass @use/@import) can reach the same file through the package
specifier instead, which survives a hoisted or monorepo install the raw
path above doesn't:
@use "@eunomia/elements/overlay-ssr.css";
Link button-ssr.css as well to draw the trigger as a button before the
upgrade, its label attribute included (see the
Button page).
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"
value="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;
}
The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.
| Variable | Sets |
|---|---|
--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 |
--dropdown-menu-focus-outline-color |
Sets the keyboard focus outline color of an option, falling back to --dropdown-menu-accent-color. 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 |
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. - With
selectable, every entry that's a choice getsrole="menuitemradio"andaria-checked, while an entry opening a submenu stays a plainmenuitem.
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