Skip to content

Dropdown menu

wu-dropdown is a menu of links and buttons in a native popover. A button with popovertarget opens and closes it, with no JavaScript. Esc and a click outside close it. Button items close it too when they point at the menu with popovertargetaction="hide".

How to make a dropdown menu without JavaScript

Section titled “How to make a dropdown menu without JavaScript”

Make the menu a <div> with the popover attribute, then give a button the menu’s id in popovertarget. Clicking the button opens the menu, and a click outside or the Esc key closes it. This is the HTML popover feature built into the browser. The menu opens under the button. The full demo is under Default.

<button class="wu-btn" type="button" popovertarget="my-menu">Actions</button>
<div class="wu-dropdown" id="my-menu" popover>
<a class="wu-dropdown-item" href="/edit">Edit</a>
<a class="wu-dropdown-item" href="/share">Share</a>
</div>
Class, attribute or variable Effect
wu-dropdown Root, on an element with popover. Raised panel with a border and a shadow
wu-dropdown-item Part, on <a> or <button>. Full-width row with a hover tint and an inset focus ring
wu-dropdown-separator Part, on <hr>. Line between groups of items
svg in an item The app’s own icon, sized to the text
danger on an item Danger text and tint
disabled or aria-disabled="true" on an item Dimmed, not clickable
popovertarget with popovertargetaction="hide" on a button item Closes the menu after the item’s click handler runs
inline-end Aligns the menu’s inline end with the trigger’s, where anchor positioning exists
animated Fades and slides in and out, opt-in
--wu-dropdown--MinWidth Minimum width, 10rem by default
--wu-dropdown--MaxHeight Height after which the menu scrolls, 20rem by default
<button class="wu-btn" type="button" popovertarget="dropdown-actions">Actions</button>
<div class="wu-dropdown" id="dropdown-actions" popover>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-actions" popovertargetaction="hide">Edit</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-actions" popovertargetaction="hide">Duplicate</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-actions" popovertargetaction="hide" disabled>Archive</button>
<hr class="wu-dropdown-separator">
<button class="wu-dropdown-item danger" type="button" popovertarget="dropdown-actions" popovertargetaction="hide">Delete</button>
</div>

Add caret to the trigger button. The caret turns while the menu is open, and an empty caret button works as a compact trigger.

<div class="flex wrap x-center gap-2">
<button class="wu-btn caret" type="button" popovertarget="dropdown-caret">Options</button>
<div class="wu-dropdown" id="dropdown-caret" popover>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-caret" popovertargetaction="hide">Rename</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-caret" popovertargetaction="hide">Move</button>
</div>
<button class="wu-btn caret" type="button" popovertarget="dropdown-caret-only" aria-label="More options"></button>
<div class="wu-dropdown" id="dropdown-caret-only" popover>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-caret-only" popovertargetaction="hide">Share</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-caret-only" popovertargetaction="hide">Copy link</button>
</div>
</div>

commandfor with command="toggle-popover" works as the trigger too.

<button class="wu-btn" type="button" commandfor="dropdown-sort" command="toggle-popover">Sort</button>
<div class="wu-dropdown" id="dropdown-sort" popover>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-sort" popovertargetaction="hide">Newest first</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-sort" popovertargetaction="hide">Oldest first</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-sort" popovertargetaction="hide">Name</button>
</div>

The menu opens under its trigger, aligned to its inline start, and flips above or to the other side when there is no room. inline-end aligns it to the trigger’s inline end instead, as the split button does.

The alignment follows the menu’s own direction, so a menu inside a dir="rtl" section of a left-to-right page opens toward the left, like the trigger’s text.

<button class="wu-btn" type="button" popovertarget="menu1" style="anchor-name: --menu1">Actions</button>
<div class="wu-dropdown" id="menu1" popover style="position-anchor: --menu1">...</div>

An svg placed directly in an item is sized to the text and keeps its own fill or stroke. Mark it aria-hidden="true": the item text names the action.

<button class="wu-btn" type="button" popovertarget="dropdown-icons">File</button>
<div class="wu-dropdown" id="dropdown-icons" popover>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-icons" popovertargetaction="hide"><svg viewBox="0 0 16 16" fill="none" stroke="currentcolor" stroke-width="1.5" aria-hidden="true"><path d="M8 2v9M4 7l4 4 4-4M3 14h10"/></svg>Download</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-icons" popovertargetaction="hide">Rename</button>
</div>

The menu aligns to the trigger’s inline start, which is its right edge in right-to-left text, and the item text and icons follow the direction.

<div dir="rtl">
<button class="wu-btn caret" type="button" popovertarget="dropdown-rtl">خيارات</button>
<div class="wu-dropdown" id="dropdown-rtl" popover>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-rtl" popovertargetaction="hide">إعادة تسمية</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-rtl" popovertargetaction="hide">نقل</button>
<hr class="wu-dropdown-separator">
<button class="wu-dropdown-item danger" type="button" popovertarget="dropdown-rtl" popovertargetaction="hide">حذف</button>
</div>
</div>

The animated word fades the menu in and slides it a little away from its trigger, down under it or up when it flips above, and reverses on close. wu-animated on an ancestor turns it on for every dropdown inside. Motion runs only under prefers-reduced-motion: no-preference.

<button class="wu-btn" type="button" popovertarget="dropdown-animated">Account</button>
<div class="wu-dropdown animated" id="dropdown-animated" popover>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-animated" popovertargetaction="hide">Profile</button>
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-animated" popovertargetaction="hide">Settings</button>
<hr class="wu-dropdown-separator">
<button class="wu-dropdown-item" type="button" popovertarget="dropdown-animated" popovertargetaction="hide">Sign out</button>
</div>
  • Items are real links and buttons: Tab moves between them, Enter activates them. The kit adds no arrow-key handling.
  • A button item with popovertargetaction="hide" closes the menu after it runs, and focus returns to the trigger. Its click listeners still fire first.
  • The trigger keeps its hover look while the menu is open; a caret trigger turns its caret.
  • Use a <button> as the trigger, so popovertarget or commandfor links it to the menu.
  • Keyboard focus draws an inset ring, so the scrolling menu does not clip it.
@use "@wovenui/components/dropdown";

The dropdown needs no other component. The examples above also use the button.