⚠️ Design system CSS not found. Check the path in cms/docs.config.js → designSystemPath, then re-run npm run docgen.

Dropdown

Contextual menus and action lists

Website / Dropdown
Download .md file
Open .md in new tab
On this page
  • Trigger patterns
  • When to use a chevron
  • Placement
  • Item patterns
  • Section labels
  • Disabled item
  • Checkable items
  • When to use what
  • JavaScript
  • Keyboard interactions
  • Accessibility
  • Structure reference
  • Usage rules
  • CSS reference
  • Use in another product

Dropdowns show a contextual menu when a trigger is clicked. They are the universal menu pattern, used in the site header, bars, and page content. One component, same classes everywhere.


Trigger patterns

The trigger is the element that opens the dropdown. provides built-in styling, no additional classes needed.

Icon + text + chevron

The most common trigger. The rotates 180 degrees when the dropdown is open.

<div class="dropdown">
  <button class="dropdown-trigger" type="button" aria-haspopup="true" aria-expanded="false">
    <div class="svg-icn" data-icon="user"><!-- svg --></div>
    <span>Account</span>
    <div class="svg-icn dropdown-chevron" data-icon="chevron-down"><!-- svg --></div>
  </button>
  <div class="dropdown-menu" role="menu">
    <!-- items -->
  </div>
</div>

Icon-only trigger

For bars and compact contexts. No text, no chevron. The icon alone signals "click for options."

<button class="dropdown-trigger" type="button" aria-haspopup="true" aria-expanded="false" aria-label="More options">
  <div class="svg-icn" data-icon="more-horizontal"><!-- svg --></div>
</button>

Button trigger

For page content where the trigger should look like a standard button.

<button class="dropdown-trigger button" data-variant="outline" type="button" ...>
  Options
  <div class="svg-icn dropdown-chevron"><!-- chevron svg --></div>
</button>

When to use a chevron

Scenario Chevron? Why
Selector-style trigger (choosing a value) Yes Signals "pick from a list"
Navigation dropdown (Account, Profile) Yes Shows the menu expands downward
Icon-only trigger (⋯ more, settings gear) No The icon itself implies a menu
Button trigger with label Optional Use when the button looks like a selector

Placement

Menus open below the trigger, left-aligned. Set data-placement on the to ask for something else.

Value Opens
bottom-start Below, left-aligned — the default
bottom-end Below, right-aligned
top-start Above, left-aligned
top-end Above, right-aligned
<div class="dropdown" data-placement="bottom-end">

Each of the four, spread across the row so the alignment is visible. The two top-* triggers open upward.

Placement is a request, not a guarantee. On open the script measures the menu against the viewport and flips whichever axis would overflow, writing the result to data-resolved-placement — which is what the CSS actually styles. A menu near the bottom of the screen opens upward on its own; one near the right edge aligns right. Each axis flips only when the opposite side genuinely has more room, so a menu taller than the viewport stays where you asked rather than flipping into an equally bad spot.

That means you no longer have to hand-place menus to avoid overflow. still works and is read as bottom-end, but new markup should use data-placement.

To watch it happen, narrow the window until the bottom-end trigger above sits within a menu-width of the left edge — it will start opening left-aligned instead. Or open the top-start menu and scroll until there is no longer room above; it drops below. The flip re-runs on resize and scroll, not only at open, so a menu that was well placed a moment ago corrects itself rather than hanging off the edge.

It resolves per axis, and only when the opposite side genuinely has more room. A menu taller than the viewport stays where you asked rather than trading one bad position for another — there is no placement that fits, so it keeps the one you chose.

Inside a bar

The bar re-points placement for you, so don't hand-set it there. A dropdown directly inside becomes a full-height cell whose panel hangs off the bar's bottom border — the panel's own top border lands on the same pixel row as the bar's hairline and paints over it, so the two read as one line with no extra rules. Collision resolution still applies; the anchor moves, the logic does not.

Nesting

A dropdown may contain another dropdown — the bar's overflow panel demotes whole groups, dropdowns included. The machinery is nesting-aware: opening a nested dropdown keeps its ancestor panel open, closing a panel closes anything nested inside it, and the open/placement selectors use child combinators so an outer cannot paint an inner menu. Inside the bar's overflow panel a nested menu renders statically, as a disclosure — see the bar doc's Overflow section.


Item patterns

Text-only item

The simplest item, just a label with no icon.

<button class="dropdown-item" role="menuitem" type="button">Admin</button>

Icon-left item

An icon before the text reinforces the action. Use for action verbs like Edit, Download, Delete.

<button class="dropdown-item" role="menuitem" type="button">
  <div class="svg-icn" data-icon="settings"><!-- svg --></div>
  <span>Settings</span>
</button>

Icon-right item (trailing content)

Use to push trailing content to the right edge. Ideal for keyboard shortcuts, badges, status indicators, or chevrons hinting at sub-menus.

<button class="dropdown-item" role="menuitem" type="button">
  <span>Undo</span>
  <span class="dropdown-item-end"><kbd>Ctrl+Z</kbd></span>
</button>

Icon on both sides

The two are independent, so an item can carry a leading icon and trailing content at once. No extra class is needed: the item is a flex row, and takes margin-left: auto, so whatever sits between the two is pushed apart.

<button class="dropdown-item" role="menuitem" type="button">
  <div class="svg-icn" data-icon="download"><!-- svg --></div>
  <span>Download</span>
  <span class="dropdown-item-end"><kbd>Ctrl+S</kbd></span>
</button>

Keep it to one leading icon and one trailing element. A row with an icon, a label, a shortcut and a badge reads as a table, and the whole item is flattened into a single announced string by role="menu" — see Accessibility below.

Item with description

Use for supporting text below the label.

<button class="dropdown-item" role="menuitem" type="button">
  <div>
    <div>From template</div>
    <div class="dropdown-desc">Choose from pre-built project templates</div>
  </div>
</button>

Avatar item

For user menus showing the logged-in user's identity.

E
Erlen Masson
erlen@bydefault.studio

The identity row is not a menu item, and role="menu" may only own menu items, groups and separators. So the panel keeps the identity row and the divider, and role="menu" moves inward onto a wrapping only the real items.

Both circles are the avatar component — , sized with data-size rather than a one-off width. Both are aria-hidden: the name follows in the markup, so an announced stray "E" adds nothing.

The menu opens left-aligned here, which is the default. In a real header the trigger sits at the far right, and the menu should be anchored to match — put data-placement="bottom-end" on the , not on the menu. That is genuine intent rather than overflow avoidance, so it is one of the few places the attribute is worth reaching for.

<div class="dropdown-menu">
  <div class="dropdown-header">
    <span class="avatar" aria-hidden="true">E</span>
    <div>
      <div id="account-name">Erlen Masson</div>
      <div class="dropdown-desc">erlen@bydefault.studio</div>
    </div>
  </div>
  <div class="dropdown-divider" role="separator"></div>
  <div class="dropdown-group" role="menu" aria-labelledby="account-name">
    <button class="dropdown-item" role="menuitem" type="button">…</button>
  </div>
</div>

Section labels

Use for non-interactive section headers. A label cannot be a direct child of role="menu" either, so wrap each section in a with role="group" and point aria-labelledby at the label. Screen readers then announce "Actions, group, 3 items" instead of dropping the heading entirely.

Actions
Danger zone
<div class="dropdown-group" role="group" aria-labelledby="grp-actions">
  <div class="dropdown-label" id="grp-actions">Actions</div>
  <button class="dropdown-item" role="menuitem" type="button">Rename</button>
  <button class="dropdown-item" role="menuitem" type="button">Duplicate</button>
</div>
<div class="dropdown-divider" role="separator"></div>
<div class="dropdown-group" role="group" aria-labelledby="grp-danger">
  <div class="dropdown-label" id="grp-danger">Danger zone</div>
  <button class="dropdown-item dropdown-item--danger" role="menuitem" type="button">Delete project</button>
</div>

Disabled item

<button class="dropdown-item" role="menuitem" type="button" aria-disabled="true">
  Save as...
</button>

Use aria-disabled="true", not the disabled attribute. A disabled button is removed from the arrow cycle, so a keyboard user never learns the option exists while a mouse user can see it sitting there. aria-disabled keeps the item reachable and announced as unavailable, and dropdown.js refuses the activation.


Checkable items

Add role="menuitemcheckbox" with aria-checked for options that toggle. Selecting one does not close the menu — a multi-select the user has to reopen per choice is unusable. Use role="menuitemradio" inside a role="group" for one-of-N instead; the script clears the siblings for you.

<button class="dropdown-item is-selected" role="menuitemcheckbox" type="button"
        data-value="name" aria-checked="true">
  <span class="dropdown-checkmark"><!-- check icon, aria-hidden --></span>
  <span>Name</span>
</button>

The checkmark is what carries the state — a background tint alone would fail WCAG 1.4.1. Its space is reserved in both states, so toggling never reflows the row.

Reacting to a selection

Activating any item fires a bubbling dropdown-select on the .

document.querySelector('.dropdown').addEventListener('dropdown-select', (event) => {
  const { value, item, checked } = event.detail;
  // checked is true/false for checkable items, null for plain ones
});

When to use what

Pattern When to use Example
Icon-left Action verbs, the icon reinforces what the action does Edit, Download, Delete, Settings
Icon-right () Trailing metadata: keyboard shortcuts, badges, status Undo Ctrl+Z, Status Active
Both together An action that also carries metadata. One icon each side, no more Download Ctrl+S
No icon Simple value selection, the text is self-explanatory Role names, client names, sizes
Avatar () User identification, profile menus Account dropdown
Description () Items that need explanation "From template. Choose from pre-built..."
Section label () Grouping related items under a heading "Switch Role", "Danger zone"
Divider () Separating logical groups Between actions and danger items

JavaScript

Include assets/js/dropdown.js on any page. It auto-initialises all elements, no setup needed.

<script src="/assets/js/dropdown.js"></script>

Clicking a toggles on the parent . Clicking outside, moving focus away, or pressing Escape closes it.

The script also wires the ARIA relationships you would otherwise repeat on every instance: aria-controls on the trigger, aria-labelledby on the menu, and tabindex="-1" on each item while the menu is open. You author the roles; the plumbing is applied for you.


Keyboard interactions

This is the WAI-ARIA menu button pattern in full. role="menu" commits a component to all of it — arrow keys alone are not enough.

Key Action
Enter / Space / ArrowDown On the trigger: opens and focuses the first item
ArrowUp On the trigger: opens and focuses the last item
ArrowDown / ArrowUp In the menu: moves focus, wrapping from last to first and back
Home / End Jumps to the first or last item
Printable character Typeahead — focuses the next item whose label starts with what you typed. Repeating one character cycles through the items starting with it
Enter / Space On a plain item: activates it, closes the menu, returns focus to the trigger. On a checkable item: toggles it and stays open
Escape Closes and returns focus to the trigger
Tab / Shift+Tab Closes the menu and continues from the trigger

Accessibility

  • Trigger: aria-haspopup="true" and aria-expanded. The script adds aria-controls
  • Icon-only triggers: add aria-label describing the action
  • Menu: role="menu" — and it may own only menuitem, menuitemcheckbox, menuitemradio, group and separator. Any other content belongs in a outside it, or inside a
  • Items: role="menuitem" on buttons. An <a> used as an item takes role="menuitem" too, which replaces its link role — so reserve <a> items for genuine navigation and accept that they announce as menu items
  • Dividers: role="separator"
  • Disabled items: aria-disabled="true" only. Never the disabled attribute, which drops the item out of the keyboard cycle
  • Checkable items: role="menuitemcheckbox" with aria-checked on the button itself. The checkmark icon must be aria-hidden or it double-announces with the state
  • Because role="menu" puts screen readers into application mode, rich item content (, <kbd> shortcuts) is flattened into the item's accessible name. Keep item content short and meaningful when read as one string

Structure reference

Element Class Purpose
Container Positioning context
Trigger Clickable element that opens the menu
Chevron Rotating arrow indicator (on trigger)
Menu The panel that appears
Menu (right) .dropdown-menu .is-right Right-aligned from trigger
Item Clickable row
Item (danger) .dropdown-item .dropdown-item--danger Destructive action
Item (disabled) .dropdown-item[aria-disabled="true"] Unavailable action
Item (checked) .dropdown-item .is-selected Selected checkable item
Checkmark Selection indicator, space always reserved
Group role="group" or nested role="menu" wrapper
Header Non-interactive panel content, outside the menu role
Trailing content Right-aligned metadata inside an item
Description Supporting text under item label
Label Non-interactive section header, inside a group
Divider Separator line between groups

Usage rules

Do:

  • Use dropdowns for contextual actions and selections
  • Group related items with labels and dividers
  • Use icon-left for actions, icon-right for metadata
  • Add aria-label on icon-only triggers
  • Leave placement alone unless you have a reason. The default opens below and left-aligned, and the script flips it if that would overflow
  • Reach for data-placement for genuine intent — an account menu anchored to the right of a header — not to dodge an overflow the script already handles

Don't:

  • Don't use dropdowns for primary navigation, use tabs or links
  • Don't nest dropdowns inside dropdowns
  • Don't use dropdowns for form field selection, use <select> instead
  • Don't put more than 10 items in a single dropdown, break into sections or use a different pattern

CSS reference

This section documents how the component is built. For usage, see the sections above.

Tokens

Token Default What it controls
Menu background
Menu and divider border
Item hover background

Selectors

Selector Purpose
Positioning context (position: relative)
Clickable trigger: flex layout, hover styles
.dropdown-trigger:hover Trigger hover state
.dropdown-trigger .svg-icn Icon sizing inside trigger
.dropdown.is-open > .dropdown-trigger Active trigger state when menu is open
Rotating arrow indicator, transitions transform
.dropdown.is-open > .dropdown-trigger .dropdown-chevron Chevron rotated 180deg when open (child-combined so an outer open never rotates a nested one)
Absolutely positioned menu panel, hidden by default
.dropdown-menu.is-right Legacy right-alignment, read as bottom-end
.dropdown[data-resolved-placement$="-end"] > .dropdown-menu Right-aligned, after collision resolution
.dropdown[data-resolved-placement^="top"] > .dropdown-menu Opens upward, after collision resolution
.dropdown[data-resolved-placement$="-start"] > .dropdown-menu.is-right Flip overriding the legacy class
.dropdown.is-open > .dropdown-menu Visible menu state (child-combined for the same nesting reason)
Clickable row: padding, hover background
.dropdown-item:hover Item hover, uses
.dropdown-item:focus-visible Focus row tint. The ring itself comes from the global button rule
.dropdown-item .svg-icn Icon sizing inside items
.dropdown-item.is-disabled, .dropdown-item[aria-disabled="true"] Disabled item: reduced opacity, not-allowed cursor
Group wrapper, re-declares column layout
Non-interactive panel row
Selection indicator, hidden with visibility so the row cannot reflow
.dropdown-item[aria-checked="true"] .dropdown-checkmark Revealed checkmark
Destructive action, danger colour
.dropdown-item--danger:hover Danger item hover
Separator line between groups
Non-interactive section header
Supporting text below item label
Right-aligned trailing content inside an item

Use in another product

The design system installs once per product:

npm install github:bydefaultstudio/design-system-dist#semver:^4.7.0

This component's styles ship in design-system.css. Its behaviour ships as dist/js/dropdown.js — copy it into the product's served assets and include it once per page:

<script src="assets/js/dropdown.js" defer></script>

In React, render this contract through the packaged adapter instead of writing the markup by hand:

import { Dropdown } from '@bydefaultstudio/design-system/react';

The adapter renders the contract above and bridges this component's events to props — see React.

On this page
  • Trigger patterns
  • When to use a chevron
  • Placement
  • Item patterns
  • Section labels
  • Disabled item
  • Checkable items
  • When to use what
  • JavaScript
  • Keyboard interactions
  • Accessibility
  • Structure reference
  • Usage rules
  • CSS reference
  • Use in another product
Previous Accordion
Next Dialog

Was this page helpful?

We use this feedback to improve our documentation.

Thanks for your feedback

Send feedback

© 2026 By Default