Dropdown
Absolutely-positioned menus anchored to a trigger. Toggle visibility with aria-hidden or .nf-hidden on the menu.
Basic
OPEN DROPDOWN
<div class="nf-dropdown">
<button class="nf-button nf-button-ghost nf-dropdown-trigger" aria-expanded="true" aria-haspopup="menu">OPTIONS</button>
<ul class="nf-dropdown-menu" role="menu">
<li role="none"><button class="nf-dropdown-item" role="menuitem">SCAN_NODE</button></li>
<li role="none"><button class="nf-dropdown-item" role="menuitem">PURGE_CACHE</button></li>
<li role="none"><button class="nf-dropdown-item" role="menuitem" aria-disabled="true">SYNC_DATA</button></li>
</ul>
</div>With dividers and labels
GROUPED MENU
<div class="nf-dropdown">
<button class="nf-button nf-button-primary nf-dropdown-trigger" aria-expanded="true">NODE_ACTIONS</button>
<ul class="nf-dropdown-menu" role="menu">
<li class="nf-dropdown-label" role="none">DIAGNOSTICS</li>
<li role="none"><button class="nf-dropdown-item" role="menuitem">RUN_SCAN</button></li>
<li role="none"><button class="nf-dropdown-item" role="menuitem">VIEW_LOGS</button></li>
<li class="nf-dropdown-divider" role="separator"></li>
<li role="none"><button class="nf-dropdown-item nf-dropdown-item-danger" role="menuitem">TERMINATE</button></li>
</ul>
</div>Right-aligned
Add .nf-dropdown-menu-right to align the menu to the right edge of the trigger:
<ul class="nf-dropdown-menu nf-dropdown-menu-right" role="menu">
...
</ul>
JavaScript
document.querySelectorAll('.nf-dropdown').forEach((dropdown) => {
const trigger = dropdown.querySelector('.nf-dropdown-trigger');
const menu = dropdown.querySelector('.nf-dropdown-menu');
const items = () => [...menu.querySelectorAll('.nf-dropdown-item:not([aria-disabled="true"]):not(:disabled)')];
// The CSS hides the menu only for aria-hidden="true" (or .nf-hidden),
// so always write the literal string rather than toggling the attribute.
function setOpen(open, { focusTrigger = false, focusItem = open } = {}) {
trigger.setAttribute('aria-expanded', String(open));
menu.setAttribute('aria-hidden', String(!open));
if (open && focusItem) items()[0]?.focus();
else if (focusTrigger) trigger.focus();
}
// Synchronize initial ARIA state without moving focus on page load.
setOpen(trigger.getAttribute('aria-expanded') === 'true', { focusItem: false });
trigger.addEventListener('click', (e) => {
e.stopPropagation();
setOpen(trigger.getAttribute('aria-expanded') !== 'true');
});
// Arrow keys move between items; Escape closes and returns focus.
menu.addEventListener('keydown', (e) => {
const list = items();
const i = list.indexOf(document.activeElement);
if (e.key === 'ArrowDown') { e.preventDefault(); list[(i + 1) % list.length]?.focus(); }
if (e.key === 'ArrowUp') { e.preventDefault(); list[(i - 1 + list.length) % list.length]?.focus(); }
if (e.key === 'Escape') { setOpen(false, { focusTrigger: true }); }
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target)) setOpen(false);
});
});
Class reference
| Class | Description |
|---|---|
nf-dropdown |
position: relative wrapper |
nf-dropdown-trigger |
Button with caret indicator |
nf-dropdown-menu |
Absolute menu panel — hide with aria-hidden="true" or nf-hidden |
nf-dropdown-menu-right |
Right-aligns the menu |
nf-dropdown-item |
Menu row — button or anchor |
nf-dropdown-item-danger |
Error color variant |
nf-dropdown-divider |
<li> separator between item groups |
nf-dropdown-label |
<li> group header — non-interactive |