Dropdown

Action menu

Intent

Dropdown is an action menu attached to a trigger -- the "more actions" affordance on a row, a workspace switcher, a sort-by selector, an account menu in the page header. It composes on top of popover, inheriting the floating surface, anchor positioning, and entry / exit animation; the dropdown layer adds menu chrome (items, separator, optional icons / shortcuts / labels) and the small <brio-dropdown> custom element that wires arrow-key navigation, focus management, and trigger ARIA state.

Reach for popover for arbitrary floating content (hover cards, help bubbles, mini forms) -- popover doesn't manage focus or keyboard, so it's the right choice when the surface isn't a menu. Reach for sidebar-nav for persistent navigation that lives next to content. Reach for dialog when the action needs to interrupt the user. Dropdown is for transient menus of choices.

Basic usage

A dropdown wraps two things in a <brio-dropdown> element: a trigger button that invokes the menu via commandfor="id" and command="toggle-popover", and the menu panel itself -- a .popover.dropdown with role="menu" and a stable id. Items are buttons or links with role="menuitem"; the .dropdown-item class styles either form consistently. Separators are bare <hr role="separator"> elements.

brio uses commandfor + command across every overlay (dialog, popover, dropdown). The older popovertarget attribute also works for invoking popovers, but pick one per trigger -- not both. Combining them fires two invocations per click and produces stuck-state bugs across browsers. <brio-dropdown> finds its trigger by looking for [commandfor], so the menu's keyboard navigation, focus management, and trigger ARIA wiring all key off the same attribute.

<docs-resize>
    <brio-dropdown>
        <button class="button" type="button" commandfor="dd-basic" command="toggle-popover">Actions</button>

        <div class="popover dropdown" id="dd-basic" popover="" role="menu">
            <button class="dropdown-item" type="button" role="menuitem">Edit</button>
            <button class="dropdown-item" type="button" role="menuitem">Duplicate</button>
            <hr role="separator"/>
            <button class="dropdown-item is-danger" type="button" role="menuitem">Delete</button>
        </div>
    </brio-dropdown>
</docs-resize>

Open the menu with the trigger; arrow keys move focus between items; Enter or Space activates; Escape closes and returns focus to the trigger. Click outside to dismiss. The custom element wires all of this on top of the native Popover API -- there's no JavaScript in the consumer application.

Slots

.dropdown-item

A single menu row. The class styles either a <button> (for actions) or an <a> (for navigational items) -- both take role="menuitem". Items lay out as a flex row so a leading icon, growing text, and a trailing keyboard shortcut all align cleanly.

Disabled items: prefer :disabled on <button> items; use aria-disabled="true" for <a> items, where the :disabled pseudo-class doesn't apply. Both forms are skipped by the keyboard navigation and styled the same.

Icon and shortcut

Optional decorations inside an item, keyed by element. A first-child <svg> or <img> inside a .dropdown-item picks up icon styling -- fixed --icon-size, muted color, inheriting the row's color in the .is-danger variant. A <kbd> inside the item gets pushed to the trailing edge via margin-inline-start: auto and renders in the monospace font; use it for keyboard hints.

.dropdown-label

A small uppercase header that groups items below it. Useful in longer menus where a flat list would be hard to scan. The label isn't an item; it doesn't take role="menuitem" and isn't reachable by arrow keys.

Separator

A bare <hr role="separator">. Either the <hr> tag or any element with role="separator" renders as a divider line. Pure presentational; the role gives it the right semantics so screen readers announce a divider rather than a horizontal rule.

<docs-resize>
    <brio-dropdown>
        <button class="button" type="button" commandfor="dd-full" command="toggle-popover">File</button>

        <div class="popover dropdown" id="dd-full" popover="" role="menu">
            <p class="dropdown-label">File</p>
            <button class="dropdown-item" type="button" role="menuitem">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                    <polyline points="14 2 14 8 20 8"></polyline>
                </svg>
                New
                <kbd>Ctrl+N</kbd>
            </button>
            <button class="dropdown-item" type="button" role="menuitem">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
                    <polyline points="17 21 17 13 7 13 7 21"></polyline>
                </svg>
                Save
                <kbd>Ctrl+S</kbd>
            </button>
            <hr role="separator"/>
            <p class="dropdown-label">Sharing</p>
            <button class="dropdown-item" type="button" role="menuitem">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <circle cx="18" cy="5" r="3"></circle>
                    <circle cx="6" cy="12" r="3"></circle>
                    <circle cx="18" cy="19" r="3"></circle>
                    <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
                    <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
                </svg>
                Share
            </button>
            <button class="dropdown-item" type="button" role="menuitem" disabled="">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path>
                    <polyline points="16 6 12 2 8 6"></polyline>
                    <line x1="12" y1="2" x2="12" y2="15"></line>
                </svg>
                Export (disabled)
            </button>
            <hr role="separator"/>
            <button class="dropdown-item is-danger" type="button" role="menuitem">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <polyline points="3 6 5 6 21 6"></polyline>
                    <path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"></path>
                </svg>
                Delete
            </button>
        </div>
    </brio-dropdown>
</docs-resize>

Variants

Item state

Item state binds to ARIA and native attributes per P5 -- not .is-* classes. :disabled (button items) and aria-disabled="true" (link items) make an item unactivatable and unfocusable for keyboard navigation. aria-current="true" marks the currently-active option in menus that act as selectors -- a sort order, a theme picker, a view toggle. The .is-danger class is the one .is-* on items because it's a design choice (destructive action), not user-interactive state.

<docs-resize>
    <brio-dropdown>
        <button class="button" type="button" commandfor="dd-state" command="toggle-popover">Sort by</button>

        <div class="popover dropdown" id="dd-state" popover="" role="menu">
            <button class="dropdown-item" type="button" role="menuitem">Recently updated</button>
            <button class="dropdown-item" type="button" role="menuitem" aria-current="true">Recently created</button>
            <button class="dropdown-item" type="button" role="menuitem">Alphabetical</button>
            <button class="dropdown-item" type="button" role="menuitem" disabled="">Custom (locked)</button>
        </div>
    </brio-dropdown>
</docs-resize>

End-aligned

The default placement is below the trigger, aligned to its inline-start edge. .is-end on the .popover.dropdown panel switches to inline-end alignment -- the menu's trailing edge meets the trigger's trailing edge. Useful for triggers near the inline-end edge of a layout, where left-aligned menus would overflow. (The browser also flips automatically via position-try-fallbacks; the variant lets you express a preferred alignment up front.)

Trigger near the inline-start edge:
Trigger near the inline-end edge:
<docs-resize>
    <div class="flow">
        <div class="cluster is-between">
            <span>Trigger near the inline-start edge:</span>
            <brio-dropdown>
                <button class="button is-outlined" type="button" commandfor="dd-start-align" command="toggle-popover">Open</button>

                <div class="popover dropdown" id="dd-start-align" popover="" role="menu">
                    <button class="dropdown-item" type="button" role="menuitem">Default alignment</button>
                    <button class="dropdown-item" type="button" role="menuitem">Below, start-aligned</button>
                </div>
            </brio-dropdown>
        </div>

        <div class="cluster is-between">
            <span>Trigger near the inline-end edge:</span>
            <brio-dropdown>
                <button class="button is-outlined" type="button" commandfor="dd-end-align" command="toggle-popover">Open</button>

                <div class="popover dropdown is-end" id="dd-end-align" popover="" role="menu">
                    <button class="dropdown-item" type="button" role="menuitem">End-aligned</button>
                    <button class="dropdown-item" type="button" role="menuitem">Trailing edges meet</button>
                </div>
            </brio-dropdown>
        </div>
    </div>
</docs-resize>

Checkmark indicator (customization)

The default aria-current="true" treatment is a background tint plus primary-tinted text. For selection menus where the active option needs an explicit glyph as well -- a theme picker, a sort-by menu, a unit selector -- add an opt-in .is-checked class on the .popover.dropdown panel and a few lines of CSS in your own stylesheet:

.popover.dropdown.is-checked .dropdown-item::before {
    content: "";
    inline-size: var(--icon-size);
    flex-shrink: 0;
    text-align: center;
}
.popover.dropdown.is-checked .dropdown-item[aria-current="true"]::before {
    content: "\2713";
    color: var(--color-primary);
}

The ::before on every item reserves a leading slot at --icon-size width so non-current items still align with the current one's checkmark. This sits in the same inline-start position as the icon slot (a first-child <svg> or <img>), so menus that already use icons should either skip the pattern or render their icons inside the .dropdown-item body rather than as the first child. brio's docs site uses this rule -- the same one above, lifted into docs.css -- on the theme picker in the page header.

<docs-resize>
    <brio-dropdown>
        <button class="button" type="button" commandfor="dd-checked" command="toggle-popover">Sort by</button>

        <div class="popover dropdown is-checked" id="dd-checked" popover="" role="menu">
            <button class="dropdown-item" type="button" role="menuitem">Recently updated</button>
            <button class="dropdown-item" type="button" role="menuitem" aria-current="true">Recently created</button>
            <button class="dropdown-item" type="button" role="menuitem">Alphabetical</button>
        </div>
    </brio-dropdown>
</docs-resize>

State and interaction

The Popover API drives open / close and light dismiss; the <brio-dropdown> custom element adds the behaviors a menu needs but the platform doesn't yet provide:

  • Arrow keys / Home / End: roving tabindex across enabled items, with circular wrap. Disabled items skip.
  • Focus on open: when the menu opens, focus moves to the first enabled item.
  • Focus return: when the menu closes (Escape, click outside, item activation), focus returns to the trigger.
  • Close on item click: clicking a menuitem dismisses the menu so the action's effect is visible. The consumer's own click handler on the item still fires first; default behavior like <a href> navigation also runs -- the dropdown just closes alongside.
  • Trigger ARIA wiring: aria-haspopup="menu" is set when the element connects; aria-expanded flips between "false" and "true" as the menu opens and closes. The Popover API doesn't manage these attributes -- brio-dropdown owns them.
  • Disclosure chevron: a small CSS-drawn chevron renders after the trigger text on any .button or .nav-link that carries aria-haspopup="menu" -- which brio-dropdown sets automatically. The chevron rotates 180° when the menu opens, keying off the same aria-expanded state. Opt out with .no-chevron on the trigger -- typical for icon-only triggers (notifications bell, avatar) where a chevron next to the icon adds noise without information.
  • Close on scroll: popovers live in the top layer and visually detach from triggers inside sticky / fixed ancestors when the page scrolls. The dropdown closes on any ancestor scroll, matching the convention from native menus on macOS and Windows.

Animation

Inherited from popover: @starting-style + transition-behavior: allow-discrete, with opacity transitions fenced behind a non-Safari @supports block. Same recipe, same --duration-fast / reduced-motion handling.

In a navbar

A real app navbar mixes more than just dropdowns -- the patterns that show up alongside them in production are what stress-test cross-browser behavior. The demo below combines:

  • Links next to dropdowns -- Pricing as an <a class="nav-link"> sibling of the Products dropdown trigger, both styled to match.
  • A popover megamenu next to a dropdown -- Resources is a wide popover (not a menu) holding grouped link sections; sits beside Products which is a dropdown. Different keyboard models, different ARIA semantics, same visual neighborhood.
  • A search form -- a real <form role="search"> with an input, sharing the trailing-edge cluster with the dropdowns. Tests focus interactions when an interactive non-menu sibling is present.
  • An icon-only dropdown trigger -- notifications bell, no visible text, requires aria-label on the trigger for the screen-reader name.
  • Mixed item types in one menu -- the Account menu has <button> items (current selection, sign out) alongside <a href> items (settings, billing) all carrying .dropdown-item for shared styling.

Open multiple, click outside, press Escape, arrow-key navigate, click items, scroll the page, resize the container. Each browser should behave identically.

One platform note: when testing the megamenu's link grid on macOS, Tab will likely skip the <a> items inside it -- macOS only includes form controls in Tab order by default, unless the user has enabled full keyboard navigation in their OS accessibility settings. Windows and Linux include links in Tab order natively. The links are focusable everywhere; the difference is whether Tab visits them. Expected platform behavior, not a brio bug.

<docs-resize>
    <header class="navbar">
        <div class="cluster is-between">
            <a href="#"><strong>Acme</strong></a>

            <nav class="cluster" aria-label="Primary">
                <brio-dropdown>
                    <button class="nav-link" type="button" commandfor="nb-products" command="toggle-popover">Products</button>
                    <div class="popover dropdown" id="nb-products" popover="" role="menu">
                        <a class="dropdown-item" href="#" role="menuitem">Analytics</a>
                        <a class="dropdown-item" href="#" role="menuitem">Workflows</a>
                        <a class="dropdown-item" href="#" role="menuitem">Reports</a>
                        <hr role="separator"/>
                        <a class="dropdown-item" href="#" role="menuitem">All products</a>
                    </div>
                </brio-dropdown>

                <button class="nav-link" type="button" commandfor="nb-resources" command="toggle-popover">Resources</button>
                <div class="popover is-lg" id="nb-resources" popover="">
                    <nav aria-label="Resources">
                        <div class="cluster">
                            <div class="stack">
                                <p class="dropdown-label">Learn</p>
                                <a class="nav-link" href="#">Tutorials</a>
                                <a class="nav-link" href="#">Guides</a>
                                <a class="nav-link" href="#">Examples</a>
                            </div>
                            <div class="stack">
                                <p class="dropdown-label">Build</p>
                                <a class="nav-link" href="#">CLI</a>
                                <a class="nav-link" href="#">SDKs</a>
                                <a class="nav-link" href="#">Templates</a>
                            </div>
                            <div class="stack">
                                <p class="dropdown-label">Connect</p>
                                <a class="nav-link" href="#">Community</a>
                                <a class="nav-link" href="#">Blog</a>
                                <a class="nav-link" href="#">Changelog</a>
                            </div>
                        </div>
                    </nav>
                </div>

                <a href="#" class="nav-link">Pricing</a>
            </nav>

            <div class="cluster">
                <form class="cluster" role="search">
                    <input class="input" type="search" placeholder="Search..." aria-label="Search"/>
                </form>

                <brio-dropdown>
                    <button class="button is-ghost no-chevron" type="button" aria-label="Notifications" commandfor="nb-notifications" command="toggle-popover">
                        <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                            <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path>
                            <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path>
                        </svg>
                    </button>
                    <div class="popover dropdown is-end" id="nb-notifications" popover="" role="menu">
                        <p class="dropdown-label">Recent</p>
                        <button class="dropdown-item" type="button" role="menuitem">Build #142 succeeded</button>
                        <button class="dropdown-item" type="button" role="menuitem">Sarah commented on PR #87</button>
                        <button class="dropdown-item" type="button" role="menuitem">3 new pull requests</button>
                        <hr role="separator"/>
                        <a class="dropdown-item" href="#" role="menuitem">View all notifications</a>
                    </div>
                </brio-dropdown>

                <brio-dropdown>
                    <button class="button" type="button" commandfor="nb-account" command="toggle-popover">Account</button>
                    <div class="popover dropdown is-end" id="nb-account" popover="" role="menu">
                        <p class="dropdown-label">Signed in as</p>
                        <button class="dropdown-item" type="button" role="menuitem" aria-current="true">patrick@octetic.com</button>
                        <hr role="separator"/>
                        <a class="dropdown-item" href="#" role="menuitem">Settings</a>
                        <a class="dropdown-item" href="#" role="menuitem">Billing</a>
                        <hr role="separator"/>
                        <button class="dropdown-item is-danger" type="button" role="menuitem">Sign out</button>
                    </div>
                </brio-dropdown>
            </div>
        </div>
    </header>
</docs-resize>

Reference

  • .dropdown -- themed veneer applied alongside .popover on the menu panel. Inherits the surface, positioning, and animation from popover; remaps padding for menu-tight chrome.
  • .dropdown-item -- a menu row. Pair with role="menuitem". Intentional dual-pair: ARIA carries semantics, the class lets the same row styling apply in non-menu contexts.
  • <svg> or <img> (first child of an item) -- leading icon slot. Slot keyed by element.
  • <kbd> (direct child of an item) -- trailing keyboard hint, monospace. Slot keyed by element.
  • .dropdown-label -- uppercase group header inside the menu. Cross-context styling hook (works on <p>, <header>, <h6> depending on whether the surrounding role accepts a heading).
  • .is-danger -- destructive item variant.
  • .is-end (on the panel) -- end-aligned placement instead of start-aligned.
  • .no-chevron (on the trigger) -- suppresses the auto-applied disclosure chevron. Use on icon-only triggers where a chevron next to the icon is noise.

Not classes: <brio-dropdown> is a custom element; role="menu", role="menuitem", role="separator", aria-current, and aria-disabled are ARIA attributes; commandfor and command are the native HTML invoker attributes.

Customization

Dropdown remaps a handful of popover tokens (padding, max-width) and exposes its own scoped tokens for minimum width, the divider border, item padding, and item color (text + hover background). .is-danger on an item remaps both --dropdown-item-color and --dropdown-item-bg-hover to the danger palette so the row tints consistently across resting and hover states.

Full token list and defaults: src/css/dropdown.css.

Accessibility

  • Menu semantics are required. The panel needs role="menu"; items need role="menuitem". Without these, screen readers announce the panel as a generic group -- users lose the "menu" landmark and the navigation cue. brio's CSS still styles the items via the .dropdown-item class either way, but accessibility depends on the roles being present.
  • Disabled items: choose the right form. For <button role="menuitem">, use :disabled -- it's the canonical button state and is keyboard-skipped natively. For <a role="menuitem">, use aria-disabled="true"; links don't accept :disabled, and removing href would lose the anchor semantic. Both forms style and behave the same in brio.
  • Current selection vs. active item. Use aria-current="true" on items that represent the current state of a setting (the selected sort order, the active theme). Don't use it for the focused item -- focus is a transient keyboard state, not a persistent semantic. Screen readers announce aria-current as "current".
  • Trigger gets the right ARIA automatically. <brio-dropdown> sets aria-haspopup="menu" and manages aria-expanded on the trigger. Don't author these in markup -- the custom element overwrites them on connect.
  • Item content needs a name. An icon-only menu item without text has no accessible name. Add aria-label to the <button> if the item's intent isn't conveyed by visible text.

CSS Reference: Dropdown

Action menu inside a popover

Source: src/css/dropdown.css

TokenDefaultDescription
--dropdown-min-width10remMinimum width of the menu panel
--dropdown-paddingvar(--ui-pad-square-tight)Inner padding for the menu panel
--dropdown-divider-bordervar(--border-width-default) solid var(--color-border)Border for separator rules between groups
--popover-padding-xvar(--dropdown-padding)Horizontal padding override for the popover surface
--popover-padding-yvar(--dropdown-padding)Vertical padding override for the popover surface
--popover-max-widthnoneMax-width override, uncapped for menus
--dropdown-item-padding-xvar(--ui-pad-x-tight)Horizontal padding for menu items
--dropdown-item-padding-yvar(--ui-pad-y-compact)Vertical padding for menu items
--dropdown-item-colorvar(--color-text)Text color for menu items
--dropdown-item-bg-hovervar(--color-fill-on-float)Background color on item hover
--dropdown-item-bg-currentvar(--color-state-bg-on-float)Background color for the current selection
--dropdown-item-border-hovervar(--color-fill-on-float)Focus outline color on item hover
--dropdown-item-radiusvar(--radius-md)Border radius for menu items
SlotDescription
.dropdown-itemClickable menu row, also matched by [role="menuitem"]
.dropdown-labelGroup header inside the menu
.dropdown :is(hr, [role="separator"])hr or any element with role="separator" acts as a menu separator
ClassDescription
.dropdowncomponent root
.is-endEnd-aligned placement for triggers near the inline-end edge
.is-dangerDestructive action with danger coloring