Eunomia v1.1.0
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. For a quick sort or filter in a toolbar, where the current choice should stay marked, see Single choice below, or Multiple choice for a filter that combines several options. 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
Links to other pages in a site's navigation, rather than actions eun-nav-menu

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
fixed-colorbooleanfalseKeeps 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-labelstring—The accessible label for the trigger button, required when it has no visible label or slotted content, such as an icon only trigger
labelstring—The trigger button's label
iconEunomiaIconName—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
disabledbooleanfalseWhether 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
roundedbooleanfalseWhether the trigger button has fully rounded corners
fullbooleanfalseWhether 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
placementEunomiaDropdownPlacement'bottom-start'Where the menu opens relative to the trigger
selectablebooleanfalseTurns 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
multiplebooleanfalseLets 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
valuestring | 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]
clearablebooleanfalseLets choosing the current option again clear value, while selectable is set. Has no effect while multiple, where choosing a checked option unchecks it
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
eunchangeChangeEventFired 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 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
--dropdown-menu-focus-outline-colorSets the keyboard focus outline color of an option, falling back to --dropdown-menu-accent-color. Also inherited by slotted dropdown items
--button-border-colorSets the border color of the trigger button
--button-widthSets the trigger's width, such as 100% to stretch it like the full attribute does
--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
valuestring—The key reported when this item is chosen
keystring—Alias of value, kept for compatibility. Prefer value, since React and Vue reserve key and never pass it to the element
labelstring—The item's text content, if not using the default slot
iconEunomiaIconName—An optional leading icon
descriptionstring—Optional secondary text shown under the label or slotted content
disabledbooleanfalseWhether this item is disabled
activebooleanfalseMarks 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 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-focus-outline-colorSets the keyboard focus outline color, falling back to --dropdown-menu-accent-color
--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. 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 shared vocabulary, alongside every other component that reuses it.

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

The closed menu state only ever applies through eun-dropdown-button's own Shadow DOM (a native 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" 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.
  • With selectable, every entry that's a choice gets role="menuitemradio" and aria-checked, while an entry opening a submenu stays a plain menuitem.

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