Dropdown

A button and a panel of links and buttons that opens under it, closing on Escape or a click outside, with no script unless it is asked to open on hover.

Example

View Code
<div class="dropdown">
    <button class="button" type="button" popovertarget="account-menu" data-emphasis="medium">Account</button>
    <div id="account-menu" popover>
        <a href="#">Profile</a>
        <a href="#">Settings</a>
        <button type="button">Sign out</button>
    </div>
</div>

When to use it

A short list of things to go to or to do, hung off one button: an account menu, a row of secondary actions, the children of a nav item. For choosing a value in a form, use a select inside a field instead.

How it works

The panel carries popover and the button carries popovertarget, so the browser opens it, puts it in the top layer, closes it on Escape or a click outside, and keeps the button's expanded state current. Yeti adds the surface, the shadow, and the placement. Because the panel is in the top layer, nothing on the page can clip it, which is why a dropdown inside a nav works even though the bar is a query container.

Where anchor positioning exists the panel hangs under its own trigger, and data-side="end" lines it up with the trigger's end edge instead. The anchor name is scoped to each dropdown, so a page full of them behaves. Where anchor positioning is missing the browser centres the panel, which is plainer but still opens, closes, and reads correctly.

<div class="dropdown" data-side="end">
    <button class="button" type="button" popovertarget="row-actions" data-emphasis="low">More</button>
    <div id="row-actions" popover>
        <a href="#">Duplicate</a>
        <button type="button">Archive</button>
    </div>
</div>

Opening on hover

Click is the default and never changes. data-trigger="hover" adds opening under the pointer on top of it, and needs hover.js, loaded once with <script type="module" src="…/js/hover.js">. The module is optional: without it the attribute does nothing and the dropdown still works by click.

Hover is added only where the pointer can actually hover, so a touch screen keeps the tap. Opening waits for --yeti-dropdown-open-delay so a pointer crossing the trigger on its way somewhere else does not flash the panel open, and closing waits for --yeti-dropdown-close-delay so the gap between the trigger and the panel is forgiving. Hovering the panel itself keeps it open.

The module closes only a panel it opened itself. A panel you opened with Enter or a click stays open when the pointer wanders off it, because the deliberate act should outrank the accidental one. Pressing the trigger while hover has the panel open closes it, and a press always cancels an opening that has not happened yet, so the same gesture never does opposite things depending on how long you lingered.

<div class="dropdown" data-trigger="hover">
    <button class="button" type="button" popovertarget="products" data-emphasis="low">Products</button>
    <div id="products" popover>
        <a href="#">Overview</a>
        <a href="#">Pricing</a>
    </div>
</div>
<script type="module" src="…/js/hover.js"></script>

This module is a stopgap and says so. The interestfor attribute is this feature standardised, and it is in one engine today. When it reaches Baseline the module goes and the attribute maps to it instead.

Accessibility

The items are ordinary links and buttons, on purpose. role="menu" and role="menuitem" promise a full application menu, arrow keys and all, and a half-built one is worse than none; a disclosure panel of links is announced clearly by every screen reader and needs no script. Give the trigger a name, and let the browser handle the expanded state rather than setting aria-expanded yourself. Escape closes the panel and returns focus to the trigger, which is the browser's doing.

Opening on hover changes none of that. The module never touches focus, ARIA, or the keyboard; it only opens and closes the same popover the button already opens, and a reader who cannot use a pointer is unaffected either way.

Attributes

Attribute Type Values Default Description
data-side enum start, end start Which edge of the trigger the panel lines up with.
data-trigger enum click, hover click What opens the panel. Click always works; hover adds opening under the pointer, and needs the optional module and a pointer that hovers.

Children

  • > button[popovertarget]: exactly 1. The trigger; its popovertarget names the panel's id.
  • > [popover]: exactly 1. The panel, holding ordinary links and buttons.

Tokens

Token Description
--yeti-dropdown-surface Background of the panel.
--yeti-dropdown-radius Corner of the panel.
--yeti-dropdown-padding Padding around the items.
--yeti-dropdown-min Least width of the panel.
--yeti-shadow-md The panel's shadow.
--yeti-control-size Least height of an item.
--yeti-dropdown-open-delay How long the pointer must rest before a hover-triggered panel opens.
--yeti-dropdown-close-delay How long a hover-triggered panel waits after the pointer leaves before closing.
--yeti-color-text The panel's text.
--yeti-duration-fast How long the panel takes to open.
--yeti-ease The curve of that transition.
--yeti-space-xs Gap between an item's icon and its label, and between the trigger and the panel.
--yeti-space-sm Inline padding of an item.
--yeti-color-surface-sunken Background of an item under the pointer.

Accessibility

  • With data-trigger="hover" the module adds opening under the pointer and changes nothing else: the click, the keyboard, focus and the expanded state are all still the browser's, and a pointer that cannot hover never gets it. The trigger needs a name, and the browser sets its expanded state from popovertarget. The items are deliberately ordinary links and buttons with no menu roles: role="menu" promises arrow-key semantics that belong to application menus, and on a website a disclosure panel is both simpler and better announced. Inside a nav, put the dropdown in the li beside the link it belongs to.
Key Action
Enter / Space On the trigger, opens or closes the panel.
Tab Walks the items, which are ordinary links and buttons.
Escape Closes the panel and returns focus to the trigger.

Browser support

  • Used without guards: popover, @starting-style
  • Behind @supports: anchor positioning (fallback: the panel is centred by the user agent)

JavaScript

Optional enhancement: components/dropdown/hover.js. The component works without it.

Available since 7.0.0.