v1.6.x-dev NDS IQ v6

Multiselect - National Design System

A form field for picking multiple options from grouped lists, with selections shown as removable chips and submitted natively as a checkbox array.

Grouped Options

Checkboxes live inside labeled fieldsets and are the source of truth, submitting with the form as a native checkbox array. With the Apply button (shown here) toggles stage in the panel and commit on Apply — filter-style; drop the button and every toggle commits instantly.

Multi-option select
Select options…
<div class="nds-form-container nds-multiselect" data-multiselect-name="interests"> <div class="nds-form-header"> <label><span class="nds-label">Interests</span></label> </div> <div class="nds-form-control"> <div class="nds-form-action nds-prefix nds-dropmenu" data-multiselect-dropmenu> <button class="nds-btn nds-subtle nds-menu-btn nds-dropmenu-trigger" type="button"> <i class="nds-icon nds-hgi-menu-01" aria-hidden="true"></i> <span class="nds-label">Select</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <fieldset class="nds-form-group nds-check-group nds-dropmenu-group" data-no-auto-close> <legend class="nds-label">Technology</legend> <!-- checked = pre-selected (server-rendered restore works the same way) --> <div class="nds-form-container nds-check-container"> <div class="nds-form-header"> <label for="ms-default-ai"><span class="nds-label">AI &amp; ML</span></label> </div> <div class="nds-form-control"> <input type="checkbox" id="ms-default-ai" class="nds-check" name="interests[]" value="ai" data-label="AI &amp; ML" checked> </div> </div> <div class="nds-form-container nds-check-container"> <div class="nds-form-header"> <label for="ms-default-cloud"><span class="nds-label">Cloud</span></label> </div> <div class="nds-form-control"> <input type="checkbox" id="ms-default-cloud" class="nds-check" name="interests[]" value="cloud" data-label="Cloud"> </div> </div> <div class="nds-form-container nds-check-container"> <div class="nds-form-header"> <label for="ms-default-security"><span class="nds-label">Cybersecurity</span></label> </div> <div class="nds-form-control"> <input type="checkbox" id="ms-default-security" class="nds-check" name="interests[]" value="security" data-label="Cybersecurity"> </div> </div> </fieldset> <hr class="nds-divider"> <fieldset class="nds-form-group nds-check-group nds-dropmenu-group" data-no-auto-close> <legend class="nds-label">Design</legend> <div class="nds-form-container nds-check-container"> <div class="nds-form-header"> <label for="ms-default-ux"><span class="nds-label">UX Research</span></label> </div> <div class="nds-form-control"> <input type="checkbox" id="ms-default-ux" class="nds-check" name="interests[]" value="ux" data-label="UX Research" checked> </div> </div> <div class="nds-form-container nds-check-container"> <div class="nds-form-header"> <label for="ms-default-brand"><span class="nds-label">Brand Identity</span></label> </div> <div class="nds-form-control"> <input type="checkbox" id="ms-default-brand" class="nds-check" name="interests[]" value="brand" data-label="Brand Identity"> </div> </div> <div class="nds-form-container nds-check-container"> <div class="nds-form-header"> <label for="ms-default-motion"><span class="nds-label">Motion</span></label> </div> <div class="nds-form-control"> <input type="checkbox" id="ms-default-motion" class="nds-check" name="interests[]" value="motion" data-label="Motion"> </div> </div> </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-multiselect-action="reset" data-no-auto-close> <span class="nds-label">Reset</span> </button> <!-- Apply button = staged commit (filter UX). Omit it and every checkbox toggle commits instantly instead. --> <button class="nds-btn nds-primary nds-dropmenu-item" type="button" data-multiselect-action="apply"> <span class="nds-label">Apply</span> </button> </div> </div> </div> </div> <div class="nds-chips nds-multiselect-chips" data-multiselect-chips></div> <span class="nds-multiselect-placeholder">Select options&hellip;</span> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Validation v1.4.0+

Min/max selection constraints set on the wrapper and enforced on submit — always against the applied set, never a staged draft. Once an error shows, it clears live as the selection changes.

Select 2–4 options
Select 2–4 options…
<form class="nds-form"> <!-- data-min-checked / data-max-checked bound the checked count; data-required alone is shorthand for min = 1 --> <div class="nds-form-container nds-multiselect" data-multiselect-name="interests" data-min-checked="2" data-max-checked="4"> <!-- same markup as Grouped Options above: header label, dropmenu trigger + panel with option checkboxes, chip track, placeholder --> <div class="nds-form-footer" data-feedback-target hidden></div> </div> <button class="nds-btn nds-primary" type="submit"> <span class="nds-label">Submit</span> </button> </form>

Built-in Features

Auto-initialization

Any .nds-multiselect on the page wires up on load. The inner dropmenu boots itself so the trigger, panel, and keyboard flow work without extra JS.

Staged or Instant Commit

A footer button with data-multiselect-action="apply" switches the field to filter-style staging: toggles stage in the open panel, Apply commits them (chips, summary, change event), and closing without Apply discards. Without the button, every toggle commits instantly. Submitting mid-staging always posts and validates the applied set.

Declarative Options

data-multiselect-options builds the option rows from JSON — a flat array, a {value: label} map, or a grouped {legend: {value: label}} object — and data-multiselect-selected pre-checks values. populate(options, selected) does the same at runtime for fetched data.

Removable Chips

Selected values render as chips inside the form-control. Clicking a chip unchecks the option and fires a change event immediately, no need to reopen the panel.

Native Form Submission

Each option checkbox carries name="field[]" and its own value, so the browser posts the checked set as an array with zero extra wiring. data-multiselect-name on the wrapper auto-fills any checkbox missing a name.

Full Keyboard Navigation

Arrow keys walk the checkboxes, Space toggles, and Escape closes the panel and returns focus to the trigger. Removing a chip hands focus to the next chip, or back to the trigger when it was the last. A visible focus ring follows each option.

Server-Rendered Restore

Emit checked on the persisted options — the standard checkbox restore — and the field renders their chips on init. No hidden carrier inputs, no inline JS.

Screen-Reader Updates

The trigger button's aria-label reflects the current selection on every commit, so screen readers announce the selected values instead of the static button label. Every commit — instant toggle, Apply, chip removal, clear-all — also announces through the shared NDS live region in both English and Arabic.

Built-in Validation

Set data-required, data-min-checked, or data-max-checked on the wrapper and NDS.Forms.validateForm enforces it, anchoring the error on the field. Once an error shows, it clears live as the selection changes.

Programmatic Control

Every field exposes an instance on the DOM node with getSelected(), apply(), reset(), removeValue(), and populate(). Listen for nds:multiselect:change to react to committed selections.

Usage Guidelines

Best Practices

  • Use multiselect for form fields where users pick several values from a list of ten or more options and benefit from seeing the applied set as chips they can remove individually
  • Group related options inside <fieldset> elements with a <legend>. Groups make long lists scannable and let assistive technology announce the category
  • For a short, flat list (three to six options) without a category axis, reach for a plain Checkbox Group instead. Multiselect's overhead (dropmenu trigger, panel, chip track) adds friction when the options fit inline
  • For a single-value choice, use Radio Button or the select-mode of Dropmenu. Multiselect's chips-over-checkboxes pattern is wasted on single-pick fields
  • Give each option checkbox a name="field[]" and value so the browser posts the checked set natively — or set data-multiselect-name once on the wrapper to auto-fill missing names. Unnamed checkboxes do not post
  • For long or server-driven option lists, skip the hand-written rows: declare data-multiselect-options (grouped JSON) on the wrapper, or call populate() with fetched data. Both build the same canonical markup
  • Keep the Apply button for fields where users explore many toggles before settling (matching the Filter pattern); drop it for short lists where instant chip feedback reads better
  • Set a meaningful data-label on each checkbox so chip text stays readable when the surrounding label changes, wraps, or contains extra markup
  • Keep the placeholder short (two to four words). It shares the row with chips once any are applied, so a long placeholder fights for space
  • Add a <hr class="nds-divider"> between fieldsets in the panel to make the grouping visually clear
  • For a server-rendered form re-displaying a saved selection (after a validation error, edit screen), emit checked on the persisted options — the field renders their chips on init, no inline JS needed
  • To make the field mandatory or bound the selection size, set data-required, data-min-checked, or data-max-checked on the wrapper — NDS.Forms.validateForm enforces them like any checkbox group
  • When the number of selections carries meaning (quota, pricing tier), listen for nds:multiselect:change and show a count or validation hint outside the field rather than overloading the placeholder

Data Attributes

AttributeDescription
data-multiselect-nameSet on .nds-multiselect. Auto-fills name="name[]" on any option checkbox missing a name, so the form posts the checked set as an array. Redundant when every checkbox already carries its own name
data-chip-classSet on .nds-multiselect. Classes applied to generated chips: color variants nds-primary / nds-neutral, sizes nds-sm / nds-md / nds-lg, plus nds-rounded. Defaults to nds-primary nds-sm. See Chips
data-multiselect-dropmenuMarks the wrapping .nds-form-action.nds-prefix.nds-dropmenu as the host of the options panel
data-multiselect-chipsMarks the container that receives rendered chips. Required inside .nds-form-control
data-multiselect-optionsSet on .nds-multiselect. JSON that builds the option rows into the (otherwise empty) menu: an array (value doubles as label), a {value: label} map, or a grouped {legend: {value: label}} object rendering one fieldset per key. Omit to hand-write the rows
data-multiselect-selectedSet on .nds-multiselect alongside data-multiselect-options. JSON array of values to pre-check — the populated equivalent of the checked attribute
data-multiselect-actionSet on footer buttons. reset unchecks every option (staged-only while an Apply panel is open). apply commits the staged set and switches the whole field to staged mode by its presence — toggles stage in the open panel, closing without Apply reverts them, and a form submit mid-staging posts the applied set
data-required / data-min-checked / data-max-checkedSet on .nds-multiselect. Validation rules enforced by NDS.Forms.validateForm: data-required means at least one option; min/max bound the checked count. The error anchors on the dropdown trigger and clears live once fixed
data-labelSet on each checkbox input. Overrides the visible label text when rendering chips, useful when the visible label contains extra markup or context
data-no-auto-closeSet on the checkbox fieldset and the Reset button so the dropmenu stays open while the user toggles options or resets
data-state~="filled"Stamped on .nds-multiselect by JS when at least one option is checked. Hides the placeholder via CSS. Removed when all values are cleared

JavaScript API

The NDS.Multiselect namespace initializes all .nds-multiselect fields on load. Each instance lives on its DOM node as element.ndsMultiselect and exposes methods for programmatic control. Listen for nds:multiselect:change to react to committed selections.

// ── Initialize (auto-runs on load) ───────────────────── // Call again after injecting multiselect HTML dynamically. NDS.Multiselect.init(); // ── Re-initialize (alias of init) ───────────────────── // Picks up any .nds-multiselect elements added to the DOM // since the last init pass. Skips already-initialized roots. NDS.Multiselect.reinit(); // ── Create an instance manually ──────────────────────── // Returns the NDSMultiselect instance. Useful for markup // added after the initial page load. const field = document.querySelector('.nds-multiselect'); NDS.Multiselect.create(field); // ── Access the instance on a live field ──────────────── // Every auto-initialized root stores its instance here. const instance = field.ndsMultiselect; // ── Read the current selection ───────────────────────── instance.getSelected(); // ['ai', 'ux'] // checked option values // ── Commit, clear, or remove programmatically ────────── instance.apply(); // commit the checked set (staged mode's // Apply button calls this), emit change instance.reset(); // uncheck every option, emit change // (staged-only while an Apply panel is open) instance.removeValue('cloud'); // uncheck one option, emit change // ── Populate options at runtime ──────────────────────── // Same shapes as data-multiselect-options; replaces existing rows. // Second argument pre-checks values. Emits no change event — setup, // not input. instance.populate( { riyadh: 'Riyadh', jeddah: 'Jeddah' }, // or array / grouped object ['riyadh'] ); // ── Tear down an instance ────────────────────────────── // Releases listeners, clears the trigger aria-label, and unlocks // the field for a fresh init. Call before removing the markup // from the DOM in dynamic flows. NDS.Multiselect.destroy(field); // ── Listen for committed selection changes ───────────── // Fires on every commit: each toggle in instant mode; Apply, // chip removal, and committed reset in staged mode. field.addEventListener('nds:multiselect:change', (e) => { const { name, values, labels } = e.detail; // name: wrapper's data-multiselect-name ('' if unset) // values: array of checked option values // labels: array of display labels in the same order });
Last Modified Date: 15/07/2026 - 03:00 PM
Was this page useful?
60% of users said Yes from 2843 Feedbacks