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

PatternUse it for
Plain menuOne 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

ClassEffect
.neura-dropdown2-triggerMarks an element as a dropdown trigger. Adds the chevron arrow on buttons. Pair with aria-controls="…".
.neura-dropdown2-trigger-arrowlessOn a button trigger: suppresses the chevron arrow so it looks like a plain button.
.neura-dropdown2Base menu container. Required.
.neura-style-defaultDefault white-bg menu skin. Apply to the menu container.
.neura-dropdown2-sectionGroup of items with a separator above (after the first).
.neura-dropdown2-checkboxOn a menu link: render as a multi-select item with a checkmark when .checked.
.neura-dropdown2-radioSingle-select within a <ul>. Adds a dot when .checked.
.neura-dropdown2-sub-triggerItem that itself opens a nested menu. Combine with .neura-dropdown2-trigger.
.neura-dropdown2-tailedModifier on the menu; adds a small arrow above pointing at the trigger.
.neura-dropdown2-in-headerTweak for menus anchored under .neura-header.
.neura-dropdown2-in-buttonsTweak for menus anchored under a .neura-buttons group.
.neura-dropdown2-in-toolbarTweak for menus anchored under a toolbar.
.neura-list-truncateOn a <ul> inside the menu: long item labels truncate with an ellipsis instead of wrapping.
.activeOn 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).
.disabledOn a menu item: non-clickable, muted color; the keyboard cursor skips it.

Attributes

AttributeOnEffect
aria-controlsTriggerRequired. Id of the menu element.
aria-haspopupTriggerSet to "true" at init unless the markup already provides one.
aria-expandedTriggerManaged by JS: true while menu is open.
aria-hiddenMenuManaged by JS: true when closed, false when open.
aria-checkedCheckbox / radio itemToggled by the click handler in lockstep with the .checked class.
data-dropdown2-alignmentMenu"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

MemberDescription
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.
.isOpenBoolean: true while the menu is open.
.trigger / .menuThe 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.