Navigation Dropdown

Dropdown

Accessible dropdown menu built on data-wf-dropdown . Auto-initialised on DOMContentLoaded , with full keyboard navigation, seven placement options, checkbox items, icon and shortcut details, a stay-open mode for embedded forms, and a programmatic API.

Quick Start

Wrap a trigger and a div.wf-dropdown__menu in a div.wf-dropdown marked with data-wf-dropdown . The script finds the first <button> or <a> as the trigger automatically, or you can mark one explicitly with data-wf-trigger .

HTML
<div class="wf-dropdown" data-wf-dropdown>
  <button class="wf-btn" data-wf-trigger>Options</button>
  <div class="wf-dropdown__menu" role="menu">
    <button class="wf-dropdown__item" role="menuitem" data-wf-value="edit">Edit</button>
    <button class="wf-dropdown__item" role="menuitem" data-wf-value="copy">Copy</button>
    <button class="wf-dropdown__item" role="menuitem" data-wf-value="share">Share</button>
    <div class="wf-dropdown__divider" role="separator"></div>
    <button class="wf-dropdown__item wf-dropdown__item--negative"
            role="menuitem" data-wf-value="delete">Delete</button>
  </div>
</div>

Anatomy

Items support leading icons, trailing detail text (useful for shortcuts), group labels, horizontal dividers, a disabled state, and a negative/danger variant. All of these compose freely within the same menu.

Full-featured menu

HTML
<div class="wf-dropdown" data-wf-dropdown>
  <button class="wf-btn" data-wf-trigger>Account</button>
  <div class="wf-dropdown__menu" role="menu">

    <!-- Group label -->
    <div class="wf-dropdown__label">Signed in as Alex</div>

    <!-- Item with leading icon -->
    <button class="wf-dropdown__item" role="menuitem" data-wf-value="profile">
      <i class="icon user" aria-hidden="true"></i>
      Profile
    </button>

    <!-- Item with icon + trailing shortcut -->
    <button class="wf-dropdown__item" role="menuitem" data-wf-value="settings">
      <i class="icon settings" aria-hidden="true"></i>
      Settings
      <span class="wf-dropdown__item-details">Ctrl+,</span>
    </button>

    <!-- Divider -->
    <div class="wf-dropdown__divider" role="separator"></div>

    <!-- Disabled item -->
    <button class="wf-dropdown__item" role="menuitem" disabled>Admin panel</button>

    <!-- Negative/danger item -->
    <button class="wf-dropdown__item wf-dropdown__item--negative"
            role="menuitem" data-wf-value="logout">Sign out</button>

  </div>
</div>

Sizes

Add a size modifier to wf-dropdown to adjust item padding and font size. wf-dropdown--block makes the trigger container full-width and stretches the menu to match.

wf-dropdown--sm

Default

wf-dropdown--lg

HTML
<div class="wf-dropdown wf-dropdown--sm" data-wf-dropdown>...</div>
<div class="wf-dropdown" data-wf-dropdown>...</div>           <!-- default -->
<div class="wf-dropdown wf-dropdown--lg" data-wf-dropdown>...</div>
HTML
<!-- Block: trigger and menu fill the container width -->
<div class="wf-dropdown wf-dropdown--block" data-wf-dropdown>
  <button class="wf-btn wf-btn--block" data-wf-trigger>Full width</button>
  <div class="wf-dropdown__menu" role="menu">
    <button class="wf-dropdown__item" role="menuitem">Option 1</button>
    <button class="wf-dropdown__item" role="menuitem">Option 2</button>
  </div>
</div>

Placement

Set data-wf-placement on the container to control which side and alignment the menu prefers. The default is auto , which opens below the trigger and flips to above when there is not enough space. The script also auto-flips horizontal alignment to end when a start-aligned menu would overflow the viewport.

bottom-start (default)

bottom

bottom-end

top-start

HTML
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="auto">...</div>
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="bottom-start">...</div>
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="bottom">...</div>
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="bottom-end">...</div>
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="top-start">...</div>
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="top">...</div>
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="top-end">...</div>

<!-- Fine-tune offset -->
<div class="wf-dropdown" data-wf-dropdown
     data-wf-distance="8" data-wf-skidding="4">...</div>

Checkboxes

Add data-wf-type="checkbox" to an item to make it a toggleable checkbox. Include data-wf-checked to pre-check it. The component sets aria-checked and emits wf:select on every toggle. Checkbox menus usually need data-wf-stay-open on the container so the menu stays open between toggles.

HTML
<div class="wf-dropdown" data-wf-dropdown data-wf-stay-open>
  <button class="wf-btn" data-wf-trigger>View options</button>
  <div class="wf-dropdown__menu" role="menu">

    <!-- Pre-checked -->
    <button class="wf-dropdown__item" role="menuitemcheckbox"
            data-wf-type="checkbox" data-wf-checked data-wf-value="name">Name</button>

    <!-- Unchecked -->
    <button class="wf-dropdown__item" role="menuitemcheckbox"
            data-wf-type="checkbox" data-wf-value="tags">Tags</button>

  </div>
</div>

Stay Open

By default, the menu closes when any item is clicked. Add data-wf-stay-open to the container to keep it open until an outside click or Escape . This is designed for menus that contain forms, inputs, or interactive widgets. You can also add data-wf-stay-open to a single item to override only that item while leaving the rest to close normally.

HTML
<!-- Keep open until outside click or Escape -->
<div class="wf-dropdown" data-wf-dropdown data-wf-stay-open>
  <button class="wf-btn" data-wf-trigger>Filter…</button>
  <div class="wf-dropdown__menu" role="menu">
    <div style="padding:.375rem 1rem .5rem">
      <input class="wf-input" type="text" placeholder="Search…">
    </div>
    <button class="wf-dropdown__item" role="menuitem">Apply</button>
  </div>
</div>

<!-- Stay-open on a single item only -->
<button class="wf-dropdown__item" role="menuitem" data-wf-stay-open>Favourite</button>

Icon trigger

The trigger can be any element — use an icon-only button for table row action menus. Pair with data-wf-placement="bottom-end" to keep the menu right-aligned. Use the native disabled attribute or data-wf-disabled on any item.

HTML
<div class="wf-dropdown" data-wf-dropdown data-wf-placement="bottom-end">
  <button class="wf-btn wf-btn-icon" data-wf-trigger aria-label="Row actions">
    <i class="icon more vertical circle" aria-hidden="true"></i>
  </button>
  <div class="wf-dropdown__menu" role="menu" data-wf-disabled>…</div>
</div>

Caret

Add wf-dropdown__caret to any inline element inside the trigger to get a rotating chevron indicator. The component toggles rotate(180deg) when the menu is open.

HTML
<button class="wf-btn" data-wf-trigger>
  Actions
  <i class="icon chevron down wf-dropdown__caret" aria-hidden="true"></i>
</button>

Data Attributes

Container wf-dropdown

Attribute Default Description
data-wf-dropdown Required. Marks the element for auto-initialization.
data-wf-placement auto Preferred direction and alignment. Values: auto, bottom-start, bottom, bottom-end, top-start, top, top-end. auto picks the side with more room.
data-wf-distance 4 Gap in px between the trigger edge and the menu. Overrides --wf-dropdown-distance when present.
data-wf-skidding 0 Horizontal offset in px along the trigger edge. Overrides --wf-dropdown-skidding when present.
data-wf-exclusive true Set to "false" to allow multiple dropdowns to be open at the same time.
data-wf-stay-open false Keep the menu open after any item click. Use for menus containing inputs or other interactive content.

Trigger data-wf-trigger

Attribute Description
data-wf-trigger Marks the trigger element explicitly. Falls back to the first <button> or <a> in the container.

Items wf-dropdown__item

Attribute Description
data-wf-value Value emitted in the wf:select event detail.value.
data-wf-type="checkbox" Makes the item a toggleable checkbox. Manages aria-checked automatically.
data-wf-checked Initially checked state for checkbox items.
data-wf-stay-open Keep the menu open after this specific item is clicked, regardless of the container setting.
data-wf-variant="negative" Danger styling — equivalent to adding the wf-dropdown__item--negative class.
disabled / data-wf-disabled Disables the item — suppresses click, emits no event, applies reduced opacity.

CSS Properties

Set these on .wf-dropdown or any ancestor to customize the menu panel. Data attributes ( data-wf-distance , data-wf-skidding ) override the CSS variables when present.

CSS
.wf-dropdown {
  --wf-dropdown-min-width: 10rem;   /* minimum menu panel width */
  --wf-dropdown-max-width: 22rem;   /* maximum menu panel width */
  --wf-dropdown-distance:  4px;     /* gap between trigger and menu */
  --wf-dropdown-skidding:  0px;     /* horizontal offset along trigger edge */
  --wf-dropdown-z:         1000;    /* z-index of the panel */
}

Programmatic API

All [data-wf-dropdown] elements are auto-initialized. You can also construct instances manually and access the instance stored on the element.

JS
import { WojoDropdown } from './wf-dropdown.js'

// Manual init with options
const el = document.querySelector('#my-dropdown')
const dd = new WojoDropdown(el, {
    placement: 'top-end',
    distance:  8,
    stayOpen:  false,
}).init()

// Methods
dd.open()
dd.close()
dd.toggle()
dd.destroy()

// Access the auto-inited instance stored on the element
const dd = document.querySelector('[data-wf-dropdown]')._wfDropdown
Keyboard
Context Key Action
Trigger focused Enter / Space / Open menu, focus first item
Open menu, focus last item
Menu open / Move focus between items
Home / End Jump to the first / last item
Enter / Space Activate focused item
Escape Close, return focus to trigger
Tab Close (focus moves naturally to the next element)

Events

All events are dispatched on the .wf-dropdown element and bubble. wf:show and wf:hide are cancelable — call e.preventDefault() to abort the open or close.

JS
const dd = document.querySelector('[data-wf-dropdown]')

// Prevent opening in a specific state
dd.addEventListener('wf:show', e => {
    if (!userHasPermission) e.preventDefault()
})

// React to item selection
dd.addEventListener('wf:select', e => {
    console.log(e.detail.value)  // value from data-wf-value
    console.log(e.detail.item)   // the clicked HTMLElement
})
Event Cancelable Detail Description
wf:show Yes Fired when open() is called before the menu becomes visible.
wf:after-show No Fired after the menu is visible and positioned.
wf:hide Yes Fired when close() is requested before the menu hides.
wf:after-hide No Fired after the menu is fully hidden and focus returned to the trigger.
wf:select No { item, value } Fired when a non-disabled item is clicked. value is the item’s data-wf-value or null.

Class Reference

All modifier classes available on .wf-dropdown

Class Description
wf-dropdown inline-block; position: relative container. All modifiers and state attributes go on this element.
wf-dropdown--sm Smaller item padding (.375rem .875rem) and font size (.875rem).
wf-dropdown--lg Larger item padding (.625rem 1.25rem) and font size (1rem).
wf-dropdown--block Makes the container display: block and the menu min-width: 100%.
wf-dropdown__menu The floating panel. Absolutely positioned, hidden by default (opacity: 0; pointer-events: none). Requires role="menu".
wf-dropdown__item Full-width flex row item. Resets all default button/link styles. Requires role="menuitem" (or menuitemcheckbox for checkboxes).
wf-dropdown__item--negative Danger/destructive item — text and hover background use --wf-danger. Equivalent to data-wf-variant="negative".
wf-dropdown__item-details Trailing detail text (shortcut, badge, etc.) pushed to the far right via margin-inline-start: auto.
wf-dropdown__label Non-interactive group heading — small caps, muted, uppercase.
wf-dropdown__divider 1px horizontal rule separator. Requires role="separator".
wf-dropdown__caret Rotating indicator inside the trigger. Applies rotate(180deg) when [data-wf-open] is present on the container.
wf-active Optional class to visually mark a pre-selected item (same hover background).
Show More