<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>
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 .
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
<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
<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>
<!-- 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
<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.
<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.
<!-- 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.
<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.
<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.
.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.
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
| 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.
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). |