v1.6.x-dev NDS IQ v6

Text Fields - National Design System

Text, number, search, email, password, textarea, and select inputs with validation, status feedback, and interactive controls

Text Input

Standard single-line text field with clear button and validation feedback

<div class="nds-form-container"> <div class="nds-form-header"> <label for="text-input-1"> <span class="nds-label">Full Name</span> </label> </div> <div class="nds-form-control"> <input type="text" id="text-input-1" class="nds-input" placeholder="Enter your full name..."> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear input" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Prefix & Suffix

Text input with inline prefix and suffix labels inside the form control

Prefix
Suffix
<div class="nds-form-container"> <div class="nds-form-header"> <label for="prefix-suffix-1"> <span class="nds-label">Label</span> </label> </div> <div class="nds-form-control"> <div class="nds-form-action nds-prefix"><span class="nds-btn nds-subtle"><span class="nds-label">Prefix</span></span></div> <input type="text" id="prefix-suffix-1" class="nds-input" placeholder="Entered text"> <div class="nds-form-action nds-suffix"><span class="nds-btn nds-subtle"><span class="nds-label">Suffix</span></span></div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Phone Number with Country Code

Compound input using a prefix-slot dropmenu as a value picker. The dropmenu's data-select-name opt-in captures the selected country code into a hidden input that ships with the form, and updates the trigger label to a compact display variant. Add nds-phone to the input itself to strip non-digits and any leading zero as the user types, since the country code is already prepended via the prefix slot.

<div class="nds-form-container"> <div class="nds-form-header"> <label for="phone-country-1"> <span class="nds-label">Phone</span> </label> </div> <div class="nds-form-control"> <div class="nds-form-action nds-prefix nds-dropmenu" data-select-name="country-code" data-select-value="+966"> <button type="button" class="nds-btn nds-subtle nds-menu-btn nds-dropmenu-trigger"> <span class="nds-label">+966</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-value="+966" data-trigger-label="+966"> <span class="nds-label">Saudi Arabia (+966)</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-value="+971" data-trigger-label="+971"> <span class="nds-label">United Arab Emirates (+971)</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-value="+973" data-trigger-label="+973"> <span class="nds-label">Bahrain (+973)</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-value="+974" data-trigger-label="+974"> <span class="nds-label">Qatar (+974)</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-value="+965" data-trigger-label="+965"> <span class="nds-label">Kuwait (+965)</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-value="+968" data-trigger-label="+968"> <span class="nds-label">Oman (+968)</span> </button> </div> </div> </div> <input type="tel" id="phone-country-1" name="phone" class="nds-input nds-phone" placeholder="00 000 0000" autocomplete="tel-national" inputmode="numeric" required> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear input" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Number Input

Numeric field with increment and decrement buttons for precise value adjustments

<div class="nds-form-container" style="--form-width: 160px"> <div class="nds-form-header"> <label for="number-input-1"> <span class="nds-label">Quantity</span> </label> </div> <div class="nds-form-control"> <div class="nds-form-action nds-prefix"><button class="nds-btn nds-secondary nds-number-increment" type="button" aria-label="Increase value"><i class="hgi hgi-stroke hgi-add-01"></i></button></div> <input type="text" id="number-input-1" class="nds-input nds-center" inputmode="numeric" value="1" min="0" max="1000" step="1" placeholder="0"> <div class="nds-form-action nds-suffix"><button class="nds-btn nds-secondary nds-number-decrement" type="button" aria-label="Decrease value"><i class="hgi hgi-stroke hgi-minus-sign"></i></button></div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Search Input

Search field with leading icon, voice input, and clear button

<div class="nds-form-container"> <div class="nds-form-header"> <label for="search-input-1"> <span class="nds-label">Search Services</span> </label> </div> <div class="nds-form-control"> <i class="nds-icon nds-hgi-search-01" aria-hidden="true"></i> <input type="text" id="search-input-1" class="nds-search-input" name="search" placeholder="Search services..."> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear search" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> <button class="nds-btn nds-subtle nds-voice-input" type="button" aria-label="Voice input"> <i class="nds-icon nds-hgi-mic-01" aria-hidden="true"></i> </button> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Email Input

Email field with leading icon and built-in format validation

<div class="nds-form-container"> <div class="nds-form-header"> <label for="email-input-1"> <span class="nds-label">Email Address</span> </label> </div> <div class="nds-form-control"> <i class="nds-icon nds-hgi-mail-01" aria-hidden="true"></i> <input type="email" id="email-input-1" class="nds-input" placeholder="Enter your email..."> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear email" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Password Input

Password field with visibility toggle and Arabic character filtering

<div class="nds-form-container"> <div class="nds-form-header"> <label for="password-input-1"> <span class="nds-label">Password</span> </label> </div> <div class="nds-form-control"> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-toggle-password" type="button" aria-label="Show password"> <i class="nds-icon nds-hgi-view-off" aria-hidden="true"></i> </button> </div> <input type="password" id="password-input-1" class="nds-input" placeholder="Enter password..." autocomplete="new-password"> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear password" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Textarea

Multi-line text entry for longer content like messages and descriptions

<div class="nds-form-container nds-textarea"> <div class="nds-form-header"> <label for="textarea-1"> <span class="nds-label">Message</span> </label> </div> <div class="nds-form-control"> <textarea id="textarea-1" class="nds-textarea" placeholder="Enter your message..." rows="4"></textarea> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Select Dropdown

Custom dropdown select with keyboard navigation and hidden input for form submission

<div class="nds-form-container nds-select"> <div class="nds-form-header"> <label for="select-input-1"> <span class="nds-label">Region</span> </label> </div> <div class="nds-form-control"> <input type="text" id="select-input-1" class="nds-input nds-select-input" placeholder="Choose an option..." readonly> <input type="hidden" name="selectValue" class="nds-select-value"> <div class="nds-select-dropdown" hidden> <div class="nds-select-options"> <button type="button" class="nds-btn nds-subtle nds-select-option" data-value=""> <span class="nds-option-text">Choose an option...</span> </button> <button type="button" class="nds-btn nds-subtle nds-select-option" data-value="option1"> <span class="nds-option-text">Option 1</span> </button> <button type="button" class="nds-btn nds-subtle nds-select-option" data-value="option2"> <span class="nds-option-text">Option 2</span> </button> <button type="button" class="nds-btn nds-subtle nds-select-option" data-value="option3"> <span class="nds-option-text">Option 3</span> </button> </div> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Native Select

Standard <select> element wrapped in the form container for native mobile picker behavior

<div class="nds-form-container nds-select"> <div class="nds-form-header"> <label for="native-select-1"> <span class="nds-label">Region</span> </label> </div> <div class="nds-form-control"> <select id="native-select-1" name="region" class="nds-input"> <option value="" disabled selected>Choose an option...</option> <option value="riyadh">Riyadh</option> <option value="makkah">Makkah</option> <option value="eastern">Eastern Province</option> <option value="asir">Asir</option> </select> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Form Status API

Set error, success, warning, or info status on any form container with automatic feedback display

<div class="nds-form-container" id="national-id-field"> <div class="nds-form-header"> <label for="national-id"> <span class="nds-label">National ID</span> </label> </div> <div class="nds-form-control"> <input type="text" id="national-id" class="nds-input" placeholder="Enter your national ID..."> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear input" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Loading State

Flip data-state="loading" on any form-container / form-group / form-control (via NDS.State.add) and forms handles the visual UX: auto-creates a [data-loading-slot] spinner in .nds-form-action (creating the slot if missing), hides sibling action buttons, and stamps data-state="loading" on the shell — .nds-btn's CSS renders the spinner. On exit the siblings restore; .nds-clear re-computes from the current input value. Zero markup burden — same UX for remote validation, autocomplete, custom fetch, anything

<!-- Author the field as usual. Forms owns the spinner shell — you don't add a <button data-loading-slot> unless you want a custom label / icon; auto-creation covers the default case. --> <div class="nds-form-container" id="username-field"> <div class="nds-form-header"> <label for="username"><span class="nds-label">Username</span></label> </div> <div class="nds-form-control"> <input type="text" id="username" class="nds-input"> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear input" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Permanent Feedback

Tips and hints that persist across validation cycles, hidden during errors and restored when cleared

<div class="nds-form-container"> <div class="nds-form-header"> <label for="username"> <span class="nds-label">Username</span> <span class="nds-info">Permanent tip shows again after error is cleared</span> </label> </div> <div class="nds-form-control"> <input type="text" id="username" class="nds-input" placeholder="Enter username..."> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" type="button" aria-label="Clear input" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </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">Use 3-20 characters, letters and numbers only</span> </span> </div> </div>

Built-in Features

What you get out of the box with zero configuration

Auto-initialization

All form inputs auto-initialize on page load. Two-way state binding syncs disabled and data-required between inputs and containers. For dynamic content, call NDS.Forms.init().

Validation

Uses HTML5 validity checks but replaces browser popups with styled inline feedback. Error messages auto-clear on blur or change once the field is corrected. Custom messages via data-error-message.

Extending Validation

Custom components hook into submit validation by stamping setCustomValidity('message') on their input (empty string when valid) — validateForm blocks the submit and shows the stamped message as-is, no forms changes needed.

Status Feedback

Four status types: error, success, warning, info. Feedback messages render into data-feedback-target elements. Permanent hints survive validation cycles with data-permanent.

Interactive States

Focus, active, typing, filled, and disabled states are tracked automatically via data-state. Clear buttons auto-show when the input has a value and hide when empty.

Password Toggle

Add a button with nds-toggle-password class to toggle between password and text input. The icon updates automatically to show the current visibility state.

Voice Input

Add a button with nds-voice-input class to enable speech-to-text on any input. Uses the Web Speech API with automatic language detection from the page.

Arabic Character Filtering

Password fields automatically strip Arabic and RTL characters on input, ensuring passwords contain only compatible characters without manual validation logic.

Number Input Controls

Add nds-number-increment and nds-number-decrement buttons as prefix/suffix actions. Supports min, max, and step attributes, with accelerated stepping on long press.

Usage Guidelines

When and how to use form inputs effectively

Best Practices

  • Use form inputs for any data collection scenario: registration flows, search interfaces, settings pages, and inline editing. Every input type shares the same container structure, so switching between text, email, password, or select requires minimal markup changes.
  • Use nds-search-input for search fields where voice input and automatic clear actions improve discoverability. The search variant includes a leading icon and optional microphone button out of the box.
  • Use the custom select dropdown (nds-select) for styled option lists that match the design system — it ships with keyboard navigation and a hidden input so it submits with the form. For native mobile picker behavior, wrap a standard <select> in the same nds-form-container nds-select shell as shown in the Native Select demo.
  • Do not use form inputs for binary choices or toggles. Use switches for instant on/off toggles, checkboxes for multiple selections, or radio buttons for single selection from a set.
  • Do not use plain text inputs for specialized data types. Use date picker for dates, OTP input for verification codes, and autocomplete for search-as-you-type with remote data.
  • Choose nds-lg (default) for standalone forms and primary data entry. Use nds-md for compact layouts like table filters, inline editing, or sidebars where space is limited.
  • Add data-required to the container rather than required on the input. The system syncs the two automatically and adds the required indicator to the label.
  • Use permanent feedback (data-permanent) for format hints and character requirements. These persist across validation cycles: they hide when an error appears and restore when the error clears.
  • Use prefix and suffix slots for units, currency symbols, or action buttons that contextualize the input. Prefix/suffix buttons support both subtle and solid styles, and can include icons or dropmenus for compound inputs.
  • For compound inputs (phone + country code, amount + currency, measurement + unit), use a prefix-slot dropmenu with data-select-name. The dropmenu renders a hidden input that submits with the form and keeps the trigger label compact while the dropdown rows stay descriptive. See the Phone Number with Country Code demo for the full pattern.
  • Always include a data-feedback-target element in the form footer, even if initially hidden. The validation system needs this target to inject error, success, and warning messages.
  • Set data-error-message on the container to override default browser validation messages with context-specific text that guides the user toward correction.

Modifier Classes

ClassElementDescription
nds-mdnds-form-containerMedium size with reduced height (32px) and smaller font
nds-lighternds-form-containerFilled-background variant using the lighter surface token. Removes the input border so the fill alone defines the field boundary. Not applied to file upload containers.
nds-darkernds-form-containerFilled-background variant using the darker surface token. Same border behavior as nds-lighter. Not applied to file upload containers.
nds-rowViewnds-form-groupLays out checkbox, radio, or switch group items in a horizontal wrapping row instead of the default vertical column.
nds-phonends-input (tel)Set on a tel input that sits next to a country-code prefix slot. On every keystroke, strips non-digit characters and any leading zero so the value submitted is the local number ready to concatenate with the prefix. Length is still controlled by the input's maxlength.

Data Attributes

AttributeDescription
data-requiredSet on nds-form-container to mark the field as required. Automatically syncs to the input's required attribute and adds the asterisk indicator.
data-error-messageSet on nds-form-container to override the default browser validation message with custom text.
data-permanentSet on a feedback element inside the footer. Permanent feedback hides during validation errors and restores when cleared.
minSet on number inputs to define the minimum allowed value (default: 0).
maxSet on number inputs to define the maximum allowed value (default: 1000).
stepSet on number inputs to define the increment/decrement size (default: 1). Long press accelerates to 10x this value.
data-select-nameSet on an nds-dropmenu (including prefix/suffix dropmenus) to turn it into a value picker. Renders a hidden <input> with this name; item clicks write the item's data-value into it and update the trigger label.
data-select-valueSet on a select-mode dropmenu to pre-select the item whose data-value matches. Takes precedence over any pre-rendered data-state="selected" item.
data-required (on dropmenu)Set on a select-mode dropmenu to add required to the hidden input, so native form submission blocks when nothing is picked.
data-trigger-labelSet on an nds-dropmenu-item to override the trigger label text when that item is chosen. Useful when the dropdown row is descriptive (Saudi Arabia (+966)) but the trigger slot needs something compact (+966).
data-min-checkedSet on nds-form-group (checkbox groups). The group is invalid unless at least this many checkboxes are checked. Also adds the required asterisk to the group label.
data-max-checkedSet on nds-form-group (checkbox groups). The group is invalid when more than this many checkboxes are checked. Defaults to the total checkbox count when absent.
data-ajaxSet on nds-form. When present, the form's submit event is intercepted and preventDefault() is called after validation passes, allowing you to handle submission via fetch or XHR without a page reload.

CSS Custom Properties

PropertyDefaultDescription
--form-width100%Controls the width of the form container
--input-size40pxHeight of the input field (32px for nds-md)
--input-radiusvar(--radius-sm)Border radius of the input field
--nds-input-size16pxSize (width and height) of the checkbox and radio control element. Size modifier classes set it to 20px (nds-md) or 24px (nds-lg). Set directly to use a custom size.

JavaScript API

The NDS.Forms API manages initialization, status feedback, state management, and form validation. For dynamically added form elements, call NDS.Forms.initializeContainer(element) to initialize new inputs.

// ── Initialization ────────────────────────────────── // All forms auto-initialize on page load NDS.Forms.init(); // Initialize a specific container (for dynamic content) NDS.Forms.initializeContainer(containerElement); // Initialize a dynamic <form> element (attaches submit validation) NDS.Forms.initForm(formElement); // ── Status API ────────────────────────────────────── // Set validation status with feedback message NDS.Forms.setStatus({ element: el, status: 'error', message: 'Required field' }); NDS.Forms.setStatus({ element: el, status: 'success', message: 'Looks good!' }); NDS.Forms.setStatus({ element: el, status: 'warning', message: 'Please review' }); NDS.Forms.setStatus({ element: el, status: 'info', message: 'Hint text' }); // Permanent feedback (survives validation cycles) NDS.Forms.setStatus({ element: el, status: 'neutral', message: 'Tip text', permanent: true }); // Clear status (restores permanent feedback if present) NDS.Forms.clearStatus(el); // Get current status NDS.Forms.getStatus(el); // Returns: { status: 'error', message: '...', isValid: false } // ── Field Sync ────────────────────────────────────── // Re-sync a form-control's chrome after programmatic value/checked changes. // Call this (or dispatch input/change) when setting a field's value from JS; // raw assignment alone no longer notifies the field. NDS.Forms.syncState(inputElement); // Set or clear a checkbox's indeterminate state. // Stamps data-state="indeterminate" on the container and fires nds:indeterminateChange. NDS.Forms.setIndeterminate(checkboxElement, true); // ── State Management ──────────────────────────────── // Set disabled or required state programmatically NDS.Forms.setState(el, 'disabled', true); NDS.Forms.setState(el, 'required', true); // ── Custom Select: programmatic value ─────────────── // The same write sequence as a user pick: display label, hidden // .nds-select-value, selected markers, input/change events (field sync), // and the selectChange dispatch. Pass the .nds-form-control or the // .nds-select-input. setValue returns false when no option carries the // value — display and submit value are never desynced. Works before the // dropdown is first built and while its menu is open. NDS.CustomSelect.setValue(formControl, 'option1'); NDS.CustomSelect.clear(formControl); // ── Form Validation ───────────────────────────────── // Validate entire form (returns { valid, invalidFields, errors }) NDS.Forms.validateForm(formElement); // Validate specific groups (used on checkbox, radio, OTP pages) NDS.Forms.validateCheckboxGroup(group); NDS.Forms.validateRadioGroup(group); NDS.Forms.validateOtpGroup(group); // ── Events ────────────────────────────────────────── // Status change on any .nds-form-container element.addEventListener('nds:statusChange', function(e) { // e.detail: { status: 'error'|'success'|...|null, message: '...'|null } }); // Form validation events on .nds-form elements form.addEventListener('nds:formValidate', function(e) { // Fires after validation runs // e.detail: { valid: true|false, invalidFields: [...], errors: [...] } }); form.addEventListener('nds:formValid', function(e) { // Fires when form passes validation on submit }); form.addEventListener('nds:formInvalid', function(e) { // Fires when form fails validation on submit // e.detail: { invalidFields: [...], errors: [...] } }); // Select dropdown change event on .nds-form-control formControl.addEventListener('selectChange', function(e) { // e.detail: { value: 'option1', text: 'Option 1' } }); // Switch toggle event on .nds-switch-element switchElement.addEventListener('switchChange', function(e) { // e.detail: { checked: true|false, value: '...', input: switchInputElement } }); // Checkbox indeterminate state change on the checkbox element checkbox.addEventListener('nds:indeterminateChange', function(e) { // e.detail: { indeterminate: true|false } }); // ── Dropmenu select-mode (prefix/suffix pickers) ──── // Any .nds-dropmenu with data-select-name captures the picked value into // a hidden input and fires these two events. dropmenu.addEventListener('nds:dropmenu:selected', function(e) { // e.detail: { dropmenu, item, value } }); // Hidden input change — use this if you only care about the new value hiddenInput.addEventListener('change', function(e) { // e.target.value holds the selected data-value });
Last Modified Date: 03/08/2026 - 01:50 AM
Was this page useful?
60% of users said Yes from 2843 Feedbacks