Dropdowns
Summary
Click-toggled menus anchored to a trigger. The trigger can be
any element marked with .neura-dropdown2-trigger
and an aria-controls attribute pointing to the menu
element. Auto-discovery binds every trigger on the page at load
time. The menu portals to <body> on show so
it escapes overflow: hidden ancestors.
When to use
| Pattern | Use it for |
|---|---|
| Plain menu | One column of clickable links / actions. |
Sectioned menu.neura-dropdown2-section | Groups of items separated by horizontal rules and an optional <strong> heading per section. |
Checkbox menu.neura-dropdown2-checkbox | Multi-select filters. Click toggles .checked and aria-checked; menu stays open. |
Radio menu.neura-dropdown2-radio | Single-select within a <ul> group. Click sets self, clears siblings. |
Submenu.neura-dropdown2-sub-trigger | Item that itself opens a nested menu (combine with .neura-dropdown2-trigger). |
Tailed.neura-dropdown2-tailed | Adds a small arrow above the menu pointing at the trigger. Used in headers. |
Examples
Click any trigger to open its menu. Auto-discovery binds every
.neura-dropdown2-trigger[aria-controls] on load,
so all the markup below is functional without any per-page JS.
Basic - button trigger
Link trigger
Sectioned menu - multiple groups
Active & disabled items
Items with icons
Checkbox menu
Radio menu
Submenu - nested dropdown
Dropdown with everything
HTML
The minimum markup contract is a trigger and a menu, linked by id:
<button class="neura-button neura-dropdown2-trigger" aria-controls="my-menu">
Menu
</button>
<div id="my-menu" class="neura-dropdown2 neura-style-default" aria-hidden="true">
<ul>
<li><a href="#">Profile</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Sign out</a></li>
</ul>
</div>
The chevron arrow on the trigger button is drawn by CSS (the
.neura-dropdown2-trigger::after rule); no inner
icon span needed unless the trigger isn't a button (e.g. a
link), in which case add your own glyph.
Sectioned menu: group items with .neura-dropdown2-section.
<button class="neura-button neura-dropdown2-trigger" aria-controls="dd-sections">
Account
</button>
<div id="dd-sections" class="neura-dropdown2 neura-style-default" aria-hidden="true">
<div class="neura-dropdown2-section">
<strong>Profile</strong>
<ul>
<li><a href="#">View profile</a></li>
<li><a href="#">Edit profile</a></li>
<li><a href="#">Notifications</a></li>
</ul>
</div>
<div class="neura-dropdown2-section">
<strong>Workspace</strong>
<ul>
<li><a href="#">Members</a></li>
<li><a href="#">Billing</a></li>
<li><a href="#">Integrations</a></li>
</ul>
</div>
<div class="neura-dropdown2-section">
<ul><li><a href="#">Sign out</a></li></ul>
</div>
</div>
Checkbox menu: clicks toggle .checked; menu stays open.
<ul>
<li><a href="#" class="neura-dropdown2-checkbox checked" aria-checked="true">Show active</a></li>
<li><a href="#" class="neura-dropdown2-checkbox checked" aria-checked="true">Show archived</a></li>
<li><a href="#" class="neura-dropdown2-checkbox" aria-checked="false">Show deleted</a></li>
<li><a href="#" class="neura-dropdown2-checkbox disabled" aria-checked="false">Show drafts (disabled)</a></li>
</ul>
Radio menu: clicks pick one within the same <ul>.
<ul>
<li><a href="#" class="neura-dropdown2-radio checked" aria-checked="true">Date</a></li>
<li><a href="#" class="neura-dropdown2-radio" aria-checked="false">Title</a></li>
<li><a href="#" class="neura-dropdown2-radio" aria-checked="false">Author</a></li>
</ul>
Submenu: an item that opens a nested menu.
<ul>
<li><a href="#">Top item</a></li>
<li>
<a href="#"
class="neura-dropdown2-sub-trigger neura-dropdown2-trigger"
aria-controls="dd-sub-1">More options</a>
</li>
<li><a href="#">Bottom item</a></li>
</ul>
<div id="dd-sub-1" class="neura-dropdown2 neura-style-default" aria-hidden="true">
<ul>
<li><a href="#">Submenu item 1</a></li>
<li><a href="#">Submenu item 2</a></li>
<li><a href="#">Submenu item 3</a></li>
</ul>
</div>
CSS classes
| Class | Effect |
|---|---|
.neura-dropdown2-trigger | Marks an element as a dropdown trigger. Adds the chevron arrow on buttons. Pair with aria-controls="…". |
.neura-dropdown2-trigger-arrowless | On a button trigger: suppresses the chevron arrow so it looks like a plain button. |
.neura-dropdown2 | Base menu container. Required. |
.neura-style-default | Default white-bg menu skin. Apply to the menu container. |
.neura-dropdown2-section | Group of items with a separator above (after the first). |
.neura-dropdown2-checkbox | On a menu link: render as a multi-select item with a checkmark when .checked. |
.neura-dropdown2-radio | Single-select within a <ul>. Adds a dot when .checked. |
.neura-dropdown2-sub-trigger | Item that itself opens a nested menu. Combine with .neura-dropdown2-trigger. |
.neura-dropdown2-tailed | Modifier on the menu; adds a small arrow above pointing at the trigger. |
.neura-dropdown2-in-header | Tweak for menus anchored under .neura-header. |
.neura-dropdown2-in-buttons | Tweak for menus anchored under a .neura-buttons group. |
.neura-dropdown2-in-toolbar | Tweak for menus anchored under a toolbar. |
.neura-list-truncate | On a <ul> inside the menu: long item labels truncate with an ellipsis instead of wrapping. |
.active | On a menu item: current/highlighted; the JS keyboard cursor uses it too. Also added by JS to the trigger while its menu is open (styling hook used by header and button chrome). |
.disabled | On a menu item: non-clickable, muted color; the keyboard cursor skips it. |
Attributes
| Attribute | On | Effect |
|---|---|---|
aria-controls | Trigger | Required. Id of the menu element. |
aria-haspopup | Trigger | Set to "true" at init unless the markup already provides one. |
aria-expanded | Trigger | Managed by JS: true while menu is open. |
aria-hidden | Menu | Managed by JS: true when closed, false when open. |
aria-checked | Checkbox / radio item | Toggled by the click handler in lockstep with the .checked class. |
data-dropdown2-alignment | Menu | "left" or "right": horizontal alignment of the tailed arrow. |
Behavior
Keyboard: while the menu is open, ArrowDown / Tab move the cursor to the next enabled item and ArrowUp / Shift+Tab to the previous one (wrapping at either end); Enter invokes the active item. Keyboard navigation moves real DOM focus (so screen readers announce each item); mouse movement re-anchors the cursor without moving focus. Closing returns focus to the trigger.
Dismissal: Esc, clicking outside the menu, or scrolling the page closes it (via the shared layer manager).
Positioning: the menu opens below the trigger, flips above when there is more room there, and end-aligns to the trigger when it would spill past the viewport edge. In RTL it aligns to the trigger's inline-start edge, mirrored spill handling included.
JavaScript
API
| Member | Description |
|---|---|
Neura.dropdown2(triggerOrMenu) | Get / create the singleton instance for a trigger element, menu element, or selector string. |
.show() | Open the menu, position it below the trigger, and push it onto the layer manager. |
.hide() | Close. |
.toggle() | Open if closed, close if open. |
.isOpen | Boolean: true while the menu is open. |
.trigger / .menu | The resolved trigger and menu elements. |
.on('show' | 'hide', fn) | Subscribe to lifecycle events. The event also bubbles from the menu element as neura-dropdown-show / neura-dropdown-hide. |
.off(event, fn) | Unsubscribe. |
Auto-init binds every .neura-dropdown2-trigger[aria-controls] on DOMContentLoaded; for static HTML you don't need any JS.
Imperative open / close from your own code:
// Open / close from a button outside the trigger.
Neura.dropdown2('#my-menu').show();
Neura.dropdown2('#my-menu').hide();
Neura.dropdown2('#my-menu').toggle();
Listen for menu-open events (e.g. lazy-load menu content):
Neura.dropdown2('#my-menu').on('show', async () => {
const menu = document.getElementById('my-menu');
if (menu.dataset.loaded) return;
const items = await fetch('/api/menu-items').then((r) => r.json());
const list = menu.querySelector('ul');
list.replaceChildren(...items.map((i) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.textContent = i.label; // server values are TEXT, never markup
a.href = i.href;
li.append(a);
return li;
}));
menu.dataset.loaded = '1';
});
For a dynamically inserted trigger, re-run auto-init:
// After inserting new triggers into the DOM:
import('@neura/js/components/dropdown2.js').then((m) => m.autoInit());
Read checkbox state on close (multi-filter pattern):
Neura.dropdown2('#filters').on('hide', () => {
const selected = [...document.querySelectorAll('#filters .neura-dropdown2-checkbox.checked')]
.map((a) => a.textContent.trim());
console.log('filters:', selected);
});
AUI compatibility
Every .aui-dropdown2* selector is aliased onto the
Neura rules. AJS.dropdown2(...) is a passthrough
to Neura.dropdown2. The legacy v1
AJS.dropDown is also routed to dropdown2 so older
plugins work unchanged.