v1.6.x-dev NDS IQ v6

Tag Input - National Design System

A free-text field that turns typed values into removable chip tags, committing on Enter or comma and submitting natively as an array.

Tag Entry

Type a value and press Enter or comma to commit it as a chip. Chips share the row with the input and wrap onto new rows as they accumulate.

<div class="nds-form-container nds-taginput" data-taginput-name="skills"> <div class="nds-form-header"> <label for="taginput-default-input"><span class="nds-label">Skills</span></label> </div> <div class="nds-form-control"> <input type="text" id="taginput-default-input" placeholder="Add a skill&hellip;"> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Server-Rendered Restore

Emit one hidden input per saved tag and the field renders their chips on load. The field name is adopted from the hidden inputs, so no wrapper attribute is needed. This demo also shows the neutral chip variant via data-chip-class.

Restored from a previous submit
<div class="nds-form-container nds-taginput" data-chip-class="nds-neutral nds-sm"> <div class="nds-form-header"> <label for="taginput-restore-input"><span class="nds-label">Interests</span></label> </div> <div class="nds-form-control"> <!-- One hidden input per saved tag: seeds the chips and donates the field name --> <input type="hidden" name="interests[]" value="Design"> <input type="hidden" name="interests[]" value="Development"> <input type="text" id="taginput-restore-input" placeholder="Add an interest&hellip;"> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Tag Limit

Cap the number of tags with a wrapper attribute. Adds past the limit are rejected with an error in the field footer, clearing as soon as a tag is removed.

Maximum 3 tags
<div class="nds-form-container nds-taginput" data-taginput-name="topics" data-max-tags="3"> <div class="nds-form-header"> <label for="taginput-max-input"><span class="nds-label">Topics</span></label> </div> <div class="nds-form-control"> <input type="text" id="taginput-max-input" placeholder="Add up to 3 topics&hellip;"> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Autocomplete Assist

Add data-url to the wrapper and the field surfaces server-backed suggestions as the user types. Picked suggestions commit as tags, free typing still works, so existing tags get reused instead of retyped. The inert spinner in the action slot shows while suggestions fetch. Add data-strict and only suggestions commit: the right shape for bounded vocabularies like assigning people or categories.

Type 2+ characters to search services
<div class="nds-form-container nds-taginput" data-taginput-name="services" data-url="/api/services" data-fetch="once" data-min-chars="2"> <div class="nds-form-header"> <label for="taginput-assist-input"><span class="nds-label">Services</span></label> </div> <div class="nds-form-control"> <input type="text" id="taginput-assist-input" autocomplete="on" placeholder="Search services&hellip;"> <div class="nds-form-action"> <!-- Inert fetch spinner: shown by Autocomplete while suggestions load --> <span class="nds-btn nds-subtle nds-icon-only nds-loading" hidden aria-hidden="true"></span> </div> </div> <!-- Permanent hint: hidden while a rejection shows, restored after --> <div class="nds-form-footer" data-feedback-target> <span class="nds-feedback nds-outline nds-sm" data-status="neutral" data-permanent> <span class="nds-feedback-icon"> <i class="nds-icon" aria-hidden="true"></i> </span> <span class="nds-feedback-message">Try "request", "license", or "permit"</span> </span> </div> </div>
Strict: only suggestions commit
<div class="nds-form-container nds-taginput" data-taginput-name="assignees" data-url="/api/users" data-name="Name" data-min-chars="2" data-strict data-empty-message="No matching people"> <div class="nds-form-header"> <label for="taginput-strict-input"><span class="nds-label">Assignees</span></label> </div> <div class="nds-form-control"> <input type="text" id="taginput-strict-input" autocomplete="on" placeholder="Search the directory&hellip;"> <div class="nds-form-action"> <span class="nds-btn nds-subtle nds-icon-only nds-loading" hidden aria-hidden="true"></span> </div> </div> <div class="nds-form-footer" data-feedback-target> <span class="nds-feedback nds-outline nds-sm" data-status="neutral" data-permanent> <span class="nds-feedback-icon"> <i class="nds-icon" aria-hidden="true"></i> </span> <span class="nds-feedback-message">Try "Ahmed" or "Sara"</span> </span> </div> </div>

Built-in Features

Auto-initialization

Any .nds-taginput on the page wires up on load. Clicking anywhere on the field puts the caret in the input.

Flexible Commit Keys

Enter or a comma commits the typed text, including the Arabic comma from Arabic keyboard layouts. Pasted text splits on separators into multiple tags, and leaving the field commits pending text instead of losing it.

Backspace to Edit

Backspace on an empty input pops the last tag back into the input as editable text rather than deleting it, so a typo never means retyping the whole value.

Form Submission and Restore

Each tag ships as an <input type="hidden" name="field[]">, so a wrapping <form> posts the tags as an array. The same hidden inputs, server-rendered, restore a saved submission on load with no inline JS.

Autocomplete Assist

Add data-url to the wrapper and typing surfaces server-backed suggestions: picking one commits it as a tag, while free typing keeps working. Add data-strict to accept only suggestions, turning the field into a searchable picker for bounded vocabularies.

Rejection Feedback

Duplicates (case-insensitive) and adds past data-max-tags are rejected with an error in the field footer, clearing on the next successful commit or removal.

Screen-Reader Updates

Every add, removal, edit, and rejection announces through the shared NDS live region in both English and Arabic, and removing a chip hands keyboard focus to the next chip instead of dropping it.

Programmatic Control

Every field exposes an instance on the DOM node with getValues(), addTag(), removeTag(), and clear(). Listen for nds:taginput:change to react to changes.

Usage Guidelines

Best Practices

  • Use tag input for open-ended, user-defined values: skills, keywords, topics, reference numbers. The vocabulary belongs to the user, not to a predefined list
  • When the options are a fixed set the user picks from, use Multiselect instead: it validates against known values and supports grouped options
  • For a single free-text value, use a plain text input. The chip ceremony adds friction when only one value is expected
  • Name the field with data-taginput-name, or let server-rendered hidden inputs donate the name. An unnamed field is UI-only and posts nothing
  • Set data-max-tags when the backend caps the list. The limit is enforced at entry with visible feedback, so users never lose work at submit time
  • Keep expected tags short, one to three words. Long values wrap awkwardly as chips and are usually a sign the field should be a textarea
  • Duplicates are rejected case-insensitively at entry. Normalize casing server-side if the stored values must be canonical
  • When tag consistency matters, enable autocomplete assist with data-url: suggestions steer users to existing tags instead of coining variants. See Autocomplete for the fetch options (data-fetch, data-min-chars, data-name)
  • When the tag count carries meaning (quota, pricing), listen for nds:taginput:change and show a counter outside the field rather than overloading the placeholder

Data Attributes

AttributeDescription
data-taginput-nameSet on .nds-taginput. Names the hidden carriers, posted as name[]. When omitted, the name is adopted from server-rendered hidden inputs; with neither, the selection does not post
data-max-tagsSet on .nds-taginput. Caps the tag count; adds past the limit are rejected with footer feedback naming the limit
data-chip-classSet on .nds-taginput. 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-urlSet on .nds-taginput to enable autocomplete assist (the input also needs autocomplete="on"). Picked suggestions commit as tags. The fetch behavior is configured with Autocomplete's own attributes
data-strictSet on .nds-taginput alongside data-url. Typed text never commits: Enter and comma show "choose from the suggestions" feedback, and only picked suggestions become tags. The programmatic addTag() API is not restricted
data-taginput-valueStamped on each chip by JS with the tag's value. Use it to target specific chips from consumer code or end-to-end tests
data-state~="filled"Stamped on .nds-taginput by JS when at least one tag exists. A styling hook for consumers; removed when the last tag is cleared

JavaScript API

The NDS.TagInput namespace initializes all .nds-taginput fields on load. Each instance lives on its DOM node as element.ndsTagInput and exposes methods for programmatic control.

// ── Initialize (auto-runs on load) ───────────────────── // Call again after injecting taginput HTML dynamically. NDS.TagInput.init(); // ── Re-initialize (alias of init) ────────────────────── // Picks up .nds-taginput elements added since the last pass. NDS.TagInput.reinit(); // ── Create an instance manually ──────────────────────── const field = document.querySelector('.nds-taginput'); NDS.TagInput.create(field); // ── Access the instance on a live field ──────────────── const instance = field.ndsTagInput; // ── Read the current tags ────────────────────────────── instance.getValues(); // ['Design', 'Development'] // ── Add, remove, or clear programmatically ───────────── // addTag trims separators and whitespace, rejects duplicates // and over-limit adds with the same feedback typing gets. instance.addTag('Accessibility'); instance.removeTag('Design'); instance.clear(); // remove every tag, emit change // ── Tear down an instance ────────────────────────────── // Releases listeners and unlocks the field for a fresh init. NDS.TagInput.destroy(field); // ── Listen for tag changes ───────────────────────────── // Fires on every add, removal, backspace-edit, and clear. field.addEventListener('nds:taginput:change', (e) => { const { name, values } = e.detail; // name: field name ('' when the field is UI-only) // values: array of current tag strings }); // ── Keyboard interactions (built in) ─────────────────── // Enter / comma commit the typed text as a tag // Backspace on an empty input pops the last tag for editing // Tab + Enter chips are buttons; Enter on a chip removes it
Last Modified Date: 15/07/2026 - 03:13 AM
Was this page useful?
60% of users said Yes from 2843 Feedbacks