Dropmenu - National Design System

A toggle-activated menu for presenting actions, navigation links, or filter controls in a compact overlay

Standard Dropmenu

Action menu for tasks like edit, duplicate, share, or delete. Use buttons for items that trigger actions on the current page

<div class="nds-dropmenu"> <button class="nds-btn nds-secondary-outline nds-dropmenu-trigger"> <i class="nds-icon nds-hgi-menu-01" aria-hidden="true"></i> <span class="nds-label">Actions</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <button class="nds-btn nds-subtle nds-dropmenu-item"> <i class="hgi hgi-stroke hgi-edit-02"></i> <span class="nds-label">Edit</span> </button> <button class="nds-btn nds-subtle nds-dropmenu-item"> <i class="nds-icon nds-hgi-copy-01"></i> <span class="nds-label">Duplicate</span> </button> <button class="nds-btn nds-subtle nds-dropmenu-item"> <i class="nds-icon nds-hgi-share-01" aria-hidden="true"></i> <span class="nds-label">Share</span> </button> <hr class="nds-divider"> <button class="nds-btn nds-subtle nds-dropmenu-item nds-destructive"> <i class="hgi hgi-stroke hgi-delete-02"></i> <span class="nds-label">Delete</span> </button> </div> </div> </div>

Navigation Dropmenu

Menu items that navigate to other pages. Use anchor elements when each item is a link rather than an action

<div class="nds-dropmenu"> <button class="nds-btn nds-secondary-outline nds-dropmenu-trigger"> <span class="nds-label">Account</span> <i class="hgi hgi-stroke hgi-user-circle"></i> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <a href="#" class="nds-btn nds-subtle nds-dropmenu-item"> <i class="hgi hgi-stroke hgi-user-account"></i> <span class="nds-label">Profile</span> </a> <a href="#" class="nds-btn nds-subtle nds-dropmenu-item"> <i class="hgi hgi-stroke hgi-settings-01"></i> <span class="nds-label">Settings</span> </a> <a href="#" class="nds-btn nds-subtle nds-dropmenu-item"> <i class="hgi hgi-stroke hgi-help-circle"></i> <span class="nds-label">Help</span> </a> <hr class="nds-divider"> <a href="#" class="nds-btn nds-subtle nds-dropmenu-item"> <i class="hgi hgi-stroke hgi-logout-01"></i> <span class="nds-label">Sign out</span> </a> </div> </div> </div>

Scrollable Menu with No Auto-Close

Use nds-dropmenu-scroll for scrollable content areas and data-no-auto-close to keep the menu open when interacting with form controls

<div class="nds-dropmenu"> <button class="nds-btn nds-neutral nds-menu-btn nds-dropmenu-trigger"> <i class="hgi hgi-stroke hgi-settings-02"></i> <span class="nds-label">Settings</span> </button> <div class="nds-dropmenu-menu" style="min-width: 220px;" hidden> <div class="nds-dropmenu-scroll"> <!-- data-no-auto-close keeps menu open on interaction --> <fieldset class="nds-dropmenu-group nds-form-group nds-check-group" data-no-auto-close> <legend class="nds-label">Notifications</legend> <div class="nds-form-container nds-switch-container"> <div class="nds-form-header"> <label for="setting-email"> <span class="nds-label">Email</span> </label> </div> <div class="nds-form-control"> <div class="nds-switch"> <input type="checkbox" id="setting-email" class="nds-switch-input" checked> <div class="nds-switch-track"> <div class="nds-switch-thumb"></div> </div> </div> </div> </div> <!-- more switches... --> </fieldset> <hr class="nds-divider"> <fieldset class="nds-dropmenu-group nds-form-group nds-check-group" data-no-auto-close> <legend class="nds-label">Display</legend> <div class="nds-form-container nds-check-container"> <div class="nds-form-header"> <label for="setting-compact"> <span class="nds-label">Compact view</span> </label> </div> <div class="nds-form-control"> <input type="checkbox" id="setting-compact" class="nds-check"> </div> </div> <!-- more checkboxes... --> </fieldset> </div> <div class="nds-dropmenu-footer"> <hr class="nds-divider"> <div class="nds-dropmenu-action"> <button class="nds-btn nds-secondary nds-dropmenu-item" type="button" data-no-auto-close> <span class="nds-label">Reset</span> </button> <button class="nds-btn nds-primary nds-dropmenu-item" type="button"> <span class="nds-label">Save</span> </button> </div> </div> </div> </div>

Dropmenu Inside Table

Row-level action menus inside tables portal on their own. The table wrapper scrolls and clips, so the menu moves to <body> on open (viewport-anchored, position: fixed), escapes the cell's clipping and stacking context, and follows the trigger as the page scrolls — no auto-close on scroll. The data-portal attribute in the example below is optional now. Keep it to force the move, or drop it and get the same result.

Name Role Status
Nora Al-Zahrani Administrator Active
Yousef Al-Harbi Editor Away
Layla Al-Qahtani Viewer Offline
<table class="nds-table"> <thead> <tr> <th>Name</th> <th>Role</th> <th>Status</th> <th></th> </tr> </thead> <tbody> <tr> <td>Nora Al-Zahrani</td> <td>Administrator</td> <td> <span class="nds-tag nds-sm" data-status="success"> <span class="nds-label">Active</span> </span> </td> <td> <!-- data-portal: escape the table cell's clipping/stacking context so the menu can extend beyond the row when opened. --> <div class="nds-dropmenu" data-portal> <button class="nds-btn nds-sm nds-subtle nds-dropmenu-trigger" aria-label="Row actions"> <i class="hgi hgi-stroke hgi-more-horizontal-circle-01"></i> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <button class="nds-btn nds-subtle nds-dropmenu-item"> <i class="hgi hgi-stroke hgi-edit-02"></i> <span class="nds-label">Edit</span> </button> <button class="nds-btn nds-subtle nds-dropmenu-item"> <i class="nds-icon nds-hgi-copy-01"></i> <span class="nds-label">Duplicate</span> </button> <hr class="nds-divider"> <button class="nds-btn nds-subtle nds-dropmenu-item nds-destructive"> <i class="hgi hgi-stroke hgi-delete-02"></i> <span class="nds-label">Delete</span> </button> </div> </div> </div> </td> </tr> <!-- more rows... --> </tbody> </table>

Select Mode

Add data-select-name to turn a dropmenu into a value picker. Each item needs data-value; the chosen value is written to a hidden input so the selection ships with a form, and the trigger label updates to reflect the current selection. Add nds-center to the .nds-dropmenu-menu to center-align every item's label — handy for compact pickers whose labels are short (numbers, currencies, states). To re-slice a live list, point the picker at a Pagination container with data-per-page-target (see Pagination · Per-page Picker).

<!-- data-select-name: field name for the hidden input data-select-value: pre-select "10" on init nds-center on the menu: each item label sits centered nds-menu-btn: caret appears via the button's own ::after --> <div class="nds-dropmenu" data-select-name="perPage" data-select-value="10"> <button class="nds-btn nds-secondary-outline nds-md nds-menu-btn nds-dropmenu-trigger" type="button"> <span class="nds-label">10</span> </button> <div class="nds-dropmenu-menu nds-center" hidden> <div class="nds-dropmenu-scroll"> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="5"><span class="nds-label">5</span></button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="10"><span class="nds-label">10</span></button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="25"><span class="nds-label">25</span></button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="50"><span class="nds-label">50</span></button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="100"><span class="nds-label">100</span></button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="250"><span class="nds-label">250</span></button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="500"><span class="nds-label">500</span></button> </div> </div> </div>

Searchable

Add data-search to inject a search box at the top of the menu. Match is case- and diacritic-insensitive, so "cafe" matches "café" and "مطار" matches "المَطار". Set a threshold with data-search="50" to skip injection when the menu holds fewer than N filterable items. Per-item data-search-value="…" appends extra tokens (English aliases next to an Arabic label, IDs, keywords). Opt an item OUT with data-search-item="false" to keep it always visible below the empty-state message. Override the "No results" text via data-search-empty="…"

<!-- data-search: always-on. Use data-search="50" to only show the box when the menu holds ≥ 50 items. Combines with SELECT MODE — the chosen item ships as `country` in the form, its Arabic label mirrors to the trigger. data-search-value adds English aliases so "Saudi" matches the Arabic row. --> <div class="nds-dropmenu" data-search data-select-name="country" data-select-value="SA"> <button class="nds-btn nds-secondary-outline nds-menu-btn nds-dropmenu-trigger" type="button"> <span class="nds-label">Choose a country</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="SA" data-search-value="Saudi Arabia KSA"> <span class="nds-label">المَملَكة العَربيَّة السَعوديَّة</span> </button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-value="AE" data-search-value="United Arab Emirates UAE"> <span class="nds-label">الإمارات العربية المتحدة</span> </button> <!-- … --> </div> </div> </div>

Delayed-open (Lazy Load)

Add data-delay="<ms>" to defer menu content until first open. On the first trigger click the button shows a loading state for the specified delay, fires nds:dropmenu:prepare so you can populate the menu, then opens. Subsequent opens skip the delay.

<!-- data-delay="1200": show loading state for 1200ms on first open, fire nds:dropmenu:prepare, then open. One-shot; removed after first use. --> <div class="nds-dropmenu" id="my-lazy-dropmenu" data-delay="1200"> <button class="nds-btn nds-secondary-outline nds-dropmenu-trigger"> <i class="hgi hgi-stroke hgi-list-view" aria-hidden="true"></i> <span class="nds-label">Load items</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <!-- Populated by nds:dropmenu:prepare handler --> </div> </div> </div>

Built-in Features

Auto-initialization

Dropmenus initialize automatically on page load and can be dynamically reinitialized using NDS.Dropmenu.reinit() for content added after initial load.

Smart Positioning

Menus stay fully visible regardless of where the trigger sits on the page, flipping direction when near screen edges.

Keyboard Navigation

Full keyboard support with arrow keys, Home, End, Tab, Escape, and special handling for input fields inside menus using Alt+Arrow combinations.

Accessibility

ARIA roles and states are applied automatically, and animations respect the user's reduced-motion preference.

Data Attributes

Menu items with data-no-auto-close stay open on click, letting users interact with checkboxes, inputs, and filter controls without interruption.

Programmatic Control

Programmatic control with instance methods open(), close(), toggle(), and custom events for state changes.

Usage Guidelines

Best Practices

  • Use dropmenus for action lists when screen space is limited and actions don't need to be immediately visible
  • Use for contextual actions that apply to a specific item, like row-level edit, delete, or share
  • Use for secondary navigation that doesn't need permanent visibility, like account or settings links
  • Use with data-no-auto-close for filter panels where users select multiple options before closing
  • Group related actions together and use <hr class="nds-divider"> to separate action groups
  • Keep menus focused with 3-8 items. If you need more, consider restructuring into multiple menus or a different pattern
  • Icons are optional but recommended for faster visual scanning when actions have clear iconic representations
  • Opt-in lazy menu: to keep a large menu out of the DOM until first open, put the .nds-dropmenu-menu inside a <template> child of the wrapper. The first click on the trigger stamps the markup, wires it, and opens it. Only for menus no other NDS component drives — a Filter or Share menu is read at page load and a Multiselect builds into its menu, so those stay live. Content inside a template is invisible to search engines and Ctrl+F
  • Don't use for primary navigation that should always be visible. Use the Header or Side Nav instead
  • Don't use for a single toggle action. A Switch or standalone Button is simpler
  • Don't use for complex multi-step forms. Use a Modal or Drawer for more space

Modifier Classes

Class Description
nds-destructive Applies destructive (red) styling to a menu item. Place destructive actions last in the menu for visual separation.
nds-dropmenu-footer Sticky footer area that stays visible while the menu content scrolls. Place outside .nds-dropmenu-scroll.
nds-dropmenu-action Action bar inside the footer for buttons like Clear and Apply. Buttons lay out side-by-side and share the row equally.
nds-dropmenu-group Groups form controls or related items inside the menu with consistent spacing.

Data Attributes

Attribute Element Description
data-no-auto-close .nds-dropmenu-item Prevents the menu from closing when that item is clicked. Use for checkboxes, inputs, and filter controls that need multiple interactions.
data-portal .nds-dropmenu Forces the menu to <body> on open. A menu that an ancestor would clip already moves there on its own, so set this only to force the move where no ancestor demands it. The portaled menu uses position: fixed and tracks its trigger on scroll (rAF-throttled) so it stays anchored without closing.
data-no-portal .nds-dropmenu Keeps the menu in place, even when an ancestor clips it. Set it when your own CSS or JavaScript needs the menu to stay a descendant of the wrapper — a rule such as .my-panel .nds-dropmenu-menu stops matching after the menu moves to <body>. The menu is then clipped by that ancestor, so use it for styling ownership, not to fix placement. It wins over data-portal.
data-anchor-cursor .nds-dropmenu Anchors the menu horizontally to the click position, opening it under the cursor like a context menu. Useful for wide triggers where the default trigger-centered placement feels disconnected from the click. Applies to mouse clicks only; keyboard and programmatic opens fall back to trigger-center.
data-select-name="<name>" .nds-dropmenu Turns the dropmenu into a value picker. A hidden <input name="..."> is appended so the selection ships with the enclosing form. Each selectable item must carry data-value. Fires nds:dropmenu:selected on selection.
data-select-value="<value>" .nds-dropmenu Pre-selects the item whose data-value matches. Takes priority over any data-state~="selected" pre-rendered on an item. Only meaningful when data-select-name is also set.
data-required .nds-dropmenu Adds required to the hidden input created by data-select-name, making the field participate in native form validation.
data-value="<value>" .nds-dropmenu-item The submitted value for this item when the dropmenu is in select mode (data-select-name). Items without data-value do not trigger selection.
data-trigger-label="<text>" .nds-dropmenu-item Custom short label written to the trigger button when this item is selected. Falls back to the item's .nds-label text. Useful when the menu row is descriptive ("Saudi Arabia (+966)") but the trigger slot needs something compact ("+966").
data-dropmenu-primary .nds-dropmenu-item Marks a menu item as the primary action. When the user presses Enter on a non-actionable menu element (not a button or anchor), the first non-disabled item with this attribute is clicked. Use to provide a default action on Enter inside embedded form controls.
data-dropmenu-no-click .nds-dropmenu Disables the trigger click handler so the dropmenu can only be opened programmatically (via instance.open() or instance.toggle()). Used by components like the date-picker that control open timing themselves.
data-dropmenu-no-keys .nds-dropmenu Disables the built-in keyboard navigation (ArrowDown/Up, Home, End, Tab) on the trigger and menu. Escape-to-close remains active. Use when a parent component owns its own keyboard handling (for example, a 2D grid like the date-picker's day cells).
data-delay="<ms>" .nds-dropmenu Enables delayed first-open (lazy-load) mode. On first trigger click the trigger shows a loading state for <ms> milliseconds, fires nds:dropmenu:prepare so a consumer can populate the menu, then opens. One-shot: the attribute is removed after the first open so subsequent opens are immediate.

Positioning & Portal

By default the menu uses position: absolute anchored to its wrapper. It scrolls with the trigger like a native <select> — no close-on-scroll, no DOM reparenting.

Placement runs on every open, in this order. First the menu goes below the trigger. If it does not fit below and the space above is larger, it flips above. If it fits neither side, it stays on the roomier one and scrolls: a menu with its own scroll region shrinks that region, so a search box or footer buttons stay in view, and a menu without one scrolls as a whole. The menu also clamps horizontally, so it never overflows the viewport.

A menu that an ancestor would trap or clip moves to <body> on open by itself. The component checks the ancestors on each open, so a menu inside a modal, a scrolling table wrapper, or a transformed card portals without any attribute. Portaled menus use position: fixed at <body> level and follow the trigger on scroll (rAF-throttled) instead of closing. Add data-portal to the wrapper only to force the move where no ancestor demands it. Add data-no-portal to refuse the move and keep the menu inside the wrapper: use it when your own CSS or JavaScript depends on that position, and accept that the ancestor then clips the menu. Never fix a clipped menu with an overflow or z-index override.

CSS Custom Properties

Property Default Description
--menu-padding 8px Inner padding of the menu container. Set on the .nds-dropmenu wrapper; JS snapshots the value onto the menu before portaling so it survives a DOM move to <body>.
--dropmenu-width min-content Explicit width of the menu. By default the menu sizes to its content. Set to a fixed value (e.g. 240px) to lock the width regardless of content.
--dropmenu-min-width max-content Minimum width of the menu. Defaults to max-content so the menu never wraps its widest item. Override inline or via CSS to enforce a fixed floor.
--dropmenu-max-width calc(100vw - 16px) Maximum width of the menu. Defaults to near-full-viewport to prevent overflow on narrow screens. Override to constrain wide menus.
--dropmenu-slide 8px Distance the menu slides during open/close animation. The direction reverses automatically when the menu flips above the trigger.

JavaScript API

// ── Auto-initialization ──────────────────────────────────── // Dropmenus initialize automatically on page load // ── Reinitialization ─────────────────────────────────────── // Call after dynamically adding new dropmenus to the page NDS.Dropmenu.reinit(); // ── Manual creation ──────────────────────────────────────── const element = document.querySelector('.nds-dropmenu'); const instance = NDS.Dropmenu.create(element); // ── Teardown ─────────────────────────────────────────────── // Call before the wrapper leaves the DOM — replacing a table row, closing // a view. An instance holds a document-level click listener, so dropping // its wrapper without this leaks the listener and the detached subtree. NDS.Dropmenu.destroy(element); // ── Static helpers ───────────────────────────────────────── // Walk up from any descendant (including portaled menus) to the wrapper const wrapper = NDS.Dropmenu.from(someChildElement); // Get the menu element for a wrapper (portal-aware) const menu = NDS.Dropmenu.menuOf(wrapper); // ── Instance methods ─────────────────────────────────────── // Access instance via element.ndsDropmenu const dropmenu = document.querySelector('.nds-dropmenu'); const instance = dropmenu.ndsDropmenu; instance.open(); // Open the menu instance.close(); // Close the menu instance.toggle(); // Toggle open/closed state instance.destroy(); // Release listeners and undo init — the wrapper survives, so references stay valid // ── Events ───────────────────────────────────────────────── // Listen for state changes document.addEventListener('nds:dropmenu:opened', (e) => { // e.detail: { dropmenu, trigger, menu, isOpen } console.log('Menu opened:', e.detail.dropmenu); }); document.addEventListener('nds:dropmenu:closed', (e) => { // e.detail: { dropmenu, trigger, menu, isOpen } console.log('Menu closed:', e.detail.dropmenu); }); // Fired when a select-mode item is chosen (data-select-name dropmenu) document.addEventListener('nds:dropmenu:selected', (e) => { // e.detail: { dropmenu, item, value } console.log('Selected value:', e.detail.value, 'Item:', e.detail.item); }); // Fired during delayed-first-open (data-delay) after the loading state, // before the menu opens. Populate menu content here. document.querySelector('.nds-dropmenu').addEventListener('nds:dropmenu:prepare', (e) => { // e.detail: { dropmenu, trigger, menu, isOpen } const menu = e.detail.menu; // Build and insert items into menu now }); // ── Keyboard navigation ──────────────────────────────────── // Trigger button: // Enter/Space: toggle menu // ArrowDown: open and focus first item // ArrowUp: open and focus last item // All three prefer the item marked data-state="active" (the menu's // current value, e.g. pagination's current page) when one exists // // Inside menu: // ArrowDown/Up: navigate items // Home/End: jump to first/last item // Tab/Shift+Tab: navigate focusable elements // Escape: close menu and return focus to trigger // // Inside input fields: // Alt+ArrowDown/Up: navigate menu items // Ctrl+Home/End: jump to first/last item
Last Modified Date: 01/09/2026 - 10:28 PM
Was this page useful?
60% of users said Yes from 2843 Feedbacks