Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

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

eun-button eun-icon · if using icons, severity, or submenus
Overview API Examples Accessibility

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 button. If clicking an option should set a value that persists as form state rather than firing an action, reach for select instead. See Alternatives below for a side by side comparison.

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.

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

Guidance

  • Use eun-dropdown-button when a single trigger should reveal several possible actions (e.g. a row's "⋮" menu: Edit, Duplicate, Delete)
  • Set aria-label on an icon-only trigger (no visible label/slot content) so it still has an accessible name, forwarded straight to the underlying button (see the Accessibility tab)
  • Reach for slotted eun-dropdown-item as soon as a row needs more than a plain string label: options is 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 eun-select is for
  • 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

NameTypeDefaultDescription
optionsArray<EunomiaDropdownOption>[]The menu options
disabled-keysstring[][]Keys of options to disable in addition to any option already marked disabled
aria-labelstringThe accessible label for the trigger button, required when it has no visible label or slotted content, such as an icon only trigger
labelstringThe trigger button's label
iconEunomiaIconNameAn 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
disabledbooleanfalseWhether 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
roundedbooleanfalseWhether 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
placementEunomiaDropdownPlacement'bottom-start'Where the menu opens relative to the trigger
openbooleanfalseWhether 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 Types.

Slots

NameDescription
(default)The trigger's label content, if not using the label property
itemsDropdown item elements, used instead of or alongside options
startAn icon or other content shown at the start of the trigger. Takes priority over the default icon from icon or severity

Events

NameTypeDescription
eunselectSelectEventFired when an option is chosen

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--dropdown-menu-backgroundSets the background color of the menu
--dropdown-menu-border-colorSets the border color of the menu
--dropdown-menu-border-radiusSets the corner radius of the menu
--dropdown-menu-shadowSets the box shadow of the menu
--dropdown-menu-colorSets the text color of the options
--dropdown-menu-description-colorSets the color of an option's description and submenu arrow
--dropdown-menu-disabled-colorSets the text color of a disabled option
--dropdown-menu-hover-backgroundSets the background color of a highlighted option
--dropdown-menu-accent-colorSets the color of the hover accent bar and the keyboard focus outline. Also inherited by slotted dropdown items
--button-border-colorSets the border color of the trigger button
--button-border-color-hoverSets 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

NameTypeDefaultDescription
keystringThe key reported when this item is chosen
labelstringThe item's text content, if not using the default slot
iconEunomiaIconNameAn optional leading icon
descriptionstringOptional secondary text shown under the label or slotted content
disabledbooleanfalseWhether this item is disabled
activebooleanfalseMarks 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 Types.

Slots

NameDescription
(default)The item's content, if not using the label property. Can hold any markup, not just text
iconReplaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty
submenuNested dropdown item elements, opened as this item's own submenu

CSS custom properties

NameDescription
--dropdown-menu-colorSets the text color of this item
--dropdown-menu-description-colorSets the color of this item's description and submenu arrow
--dropdown-menu-disabled-colorSets the text color of this item when disabled
--dropdown-menu-hover-backgroundSets the background color of this item when highlighted or when its submenu is open
--dropdown-menu-accent-colorSets the color of the hover accent bar and the keyboard focus outline
--dropdown-menu-backgroundSets the background color of this item's own submenu panel, if it has one
--dropdown-menu-border-colorSets the border color of this item's own submenu panel, if it has one
--dropdown-menu-border-radiusSets the corner radius of this item's own submenu panel, if it has one
--dropdown-menu-shadowSets the box shadow of this item's own submenu panel, if it has one

CSS shadow parts

NameDescription
itemThe 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 Variants, alongside every other component that reuses it.

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" and aria-expanded on the trigger.
  • role="menu" on the panel, role="menuitem" on each entry: real roving DOM focus (each enabled entry is individually focusable), not simulated via aria-activedescendant. A eun-dropdown-item sets role="menuitem" on itself, not on some internal element.
  • An entry with a submenu additionally gets aria-haspopup="menu" and aria-expanded, mirroring the trigger's own pattern one level down, recursively, for however deep a eun-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 options entries, 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

WAI-ARIA Authoring Practices: Menu Button Pattern
HTML Specification: Popover API