v1.6.x-dev NDS IQ v6

Sort - National Design System

A DOM-reorder engine for lists, grids, and tables. Pass an accessor, wire your triggers, and NDS.Sort handles type detection, direction cycles, accessibility attributes, and URL persistence.

Direct Mode

Each trigger selects a fixed key and direction. An empty key trigger resets to the original order. Use this mode for dropmenu options, pill groups, or any UI where every choice is explicitly labelled.

Sort by name or price
Zakat Payment

75

Passport Renewal

300

Birth Certificate

25

Identity Verification

Free

Driver License

150

Business Registration

1200

<div id="sortDirectRoot" class="nds-sort-demo-row"> <div class="nds-dropmenu"> <button type="button" class="nds-btn nds-secondary-outline nds-menu-btn nds-dropmenu-trigger"> <i class="nds-icon nds-hgi-sorting-05" aria-hidden="true"></i> <span class="nds-label">Sort</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-sort> <i class="nds-icon nds-hgi-sorting-05" aria-hidden="true"></i> <span class="nds-label">Default order</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-sort="name" data-sort-dir="asc"> <i class="nds-icon nds-hgi-sort-by-up-02" aria-hidden="true"></i> <span class="nds-label">Name A to Z</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-sort="name" data-sort-dir="desc"> <i class="nds-icon nds-hgi-sort-by-down-02" aria-hidden="true"></i> <span class="nds-label">Name Z to A</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-sort="price" data-sort-dir="asc"> <i class="nds-icon nds-hgi-sort-by-up-02" aria-hidden="true"></i> <span class="nds-label">Price low to high</span> </button> <button type="button" class="nds-btn nds-subtle nds-dropmenu-item" data-sort="price" data-sort-dir="desc"> <i class="nds-icon nds-hgi-sort-by-down-02" aria-hidden="true"></i> <span class="nds-label">Price high to low</span> </button> </div> </div> </div> </div> <div id="sortDirectItems" class="nds-grid"> <div class="nds-card nds-stroke" data-sort-name="Zakat Payment" data-sort-price="75"> <div class="nds-card-content"> <span class="nds-card-title">Zakat Payment</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">75</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Passport Renewal" data-sort-price="300"> <div class="nds-card-content"> <span class="nds-card-title">Passport Renewal</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">300</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Birth Certificate" data-sort-price="25"> <div class="nds-card-content"> <span class="nds-card-title">Birth Certificate</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">25</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Identity Verification" data-sort-price="0"> <div class="nds-card-content"> <span class="nds-card-title">Identity Verification</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR" data-free>Free</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Driver License" data-sort-price="150"> <div class="nds-card-content"> <span class="nds-card-title">Driver License</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">150</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Business Registration" data-sort-price="1200"> <div class="nds-card-content"> <span class="nds-card-title">Business Registration</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">1200</span></p> </div> </div> </div>

Cycle Mode

Each trigger owns a single key. The same trigger advances through ascending, descending, and reset across three clicks. Use this mode for column headers, pill bars, or any UI where the trigger itself signals its own sort state.

Click a trigger to cycle asc, desc, reset
Zakat Payment

75

Passport Renewal

300

Birth Certificate

25

Identity Verification

Free

Driver License

150

Business Registration

1200

<div id="sortCycleRoot"> <button type="button" class="nds-btn nds-secondary-outline nds-sort-cycle-btn" data-sort-key="name"> <span class="nds-label">Name</span> <i class="nds-icon nds-hgi-sorting-05" aria-hidden="true"></i> </button> <button type="button" class="nds-btn nds-secondary-outline nds-sort-cycle-btn" data-sort-key="price"> <span class="nds-label">Price</span> <i class="nds-icon nds-hgi-sorting-05" aria-hidden="true"></i> </button> </div> <div id="sortCycleItems" class="nds-grid"> <div class="nds-card nds-stroke" data-sort-name="Zakat Payment" data-sort-price="75"> <div class="nds-card-content"> <span class="nds-card-title">Zakat Payment</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">75</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Passport Renewal" data-sort-price="300"> <div class="nds-card-content"> <span class="nds-card-title">Passport Renewal</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">300</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Birth Certificate" data-sort-price="25"> <div class="nds-card-content"> <span class="nds-card-title">Birth Certificate</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">25</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Identity Verification" data-sort-price="0"> <div class="nds-card-content"> <span class="nds-card-title">Identity Verification</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR" data-free>Free</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Driver License" data-sort-price="150"> <div class="nds-card-content"> <span class="nds-card-title">Driver License</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">150</span></p> </div> </div> <div class="nds-card nds-stroke" data-sort-name="Business Registration" data-sort-price="1200"> <div class="nds-card-content"> <span class="nds-card-title">Business Registration</span> <p class="nds-card-description"><span class="nds-number-format" data-currency="SAR">1200</span></p> </div> </div> </div>

Table Column Sort

Drop .nds-sort-btn into any <th> and the table wires itself. NDS.Tables composes NDS.Sort in cycle mode with a cell-text accessor, so rows sort by whatever text the cell renders. Numbers like 2,500 SAR parse numerically, dates like 2026-03-15 parse chronologically, everything else falls back to locale-aware string compare.

Click any column header to sort
Name
Amount
Status
Date
Business License Renewal 2500 Completed 2026-03-15
Visa Processing Fee 800 Pending 2026-03-14
Property Transfer Tax 15000 Completed 2026-03-12
Vehicle Registration Fee 450 Completed 2026-03-10
Building Permit Application 3200 Failed 2026-03-08
Health Insurance Premium 1800 Completed 2026-03-06
<table class="nds-table nds-sortable"> <thead> <tr> <th> <div class="nds-col-header"> <span class="nds-label">Name</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by name"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th> <div class="nds-col-header"> <span class="nds-label">Amount</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by amount"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th> <div class="nds-col-header"> <span class="nds-label">Status</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by status"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> <th> <div class="nds-col-header"> <span class="nds-label">Date</span> <div class="nds-col-actions"> <button class="nds-btn nds-subtle nds-sort-btn nds-icon-only" aria-label="Sort by date"><i class="nds-icon nds-hgi-sorting-05 nds-sort-icon" aria-hidden="true"></i></button> </div> </div> </th> </tr> </thead> <tbody> <tr> <td>Business License Renewal</td> <td><span class="nds-number-format" data-currency="SAR">2500</span></td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Completed</span></span></td> <td>2026-03-15</td> </tr> <tr> <td>Visa Processing Fee</td> <td><span class="nds-number-format" data-currency="SAR">800</span></td> <td><span class="nds-tag nds-sm" data-status="warning"><span class="nds-label">Pending</span></span></td> <td>2026-03-14</td> </tr> <tr> <td>Property Transfer Tax</td> <td><span class="nds-number-format" data-currency="SAR">15000</span></td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Completed</span></span></td> <td>2026-03-12</td> </tr> <tr> <td>Vehicle Registration Fee</td> <td><span class="nds-number-format" data-currency="SAR">450</span></td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Completed</span></span></td> <td>2026-03-10</td> </tr> <tr> <td>Building Permit Application</td> <td><span class="nds-number-format" data-currency="SAR">3200</span></td> <td><span class="nds-tag nds-sm" data-status="error"><span class="nds-label">Failed</span></span></td> <td>2026-03-08</td> </tr> <tr> <td>Health Insurance Premium</td> <td><span class="nds-number-format" data-currency="SAR">1800</span></td> <td><span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Completed</span></span></td> <td>2026-03-06</td> </tr> </tbody> </table>

Built-in Features

Zero-wire from Filter and Tables

Wraps automatically when you use Filter with [data-sort] buttons or Tables with .nds-sort-btn headers. Compose directly with NDS.Sort.create() for custom widgets.

Type Auto-detect

Samples values at sort time. Formatted numbers like "9,375 SAR" sort numerically. DD/MM/YYYY, YYYY-MM-DD, and ISO 8601 sort chronologically. Everything else compares with localeCompare, so Arabic and numeric strings order correctly.

Direct and Cycle Modes

Direct mode pairs each trigger with a fixed key and direction, matching dropmenu option lists. Cycle mode advances one trigger through ascending, descending, and reset, matching column headers.

URL Persistence

Opt in with urlSync: { keyParam, dirParam } and the current sort rides the query string. The default ascending direction is omitted to keep URLs tidy, other params are preserved, and history is replaced rather than pushed.

Keyboard and ARIA

Triggers respond to Enter and Space. Direct mode writes aria-pressed on the active trigger. Cycle mode writes aria-sort on the target element (the column header by default), flipping between ascending, descending, and none.

Programmatic Control

Call sort.apply(key, dir), sort.reset(), sort.refresh(), sort.getState(), and sort.destroy() on the instance returned from create(). Items that arrive after create() — an async list, an AJAX swap — miss the initial pass, so call sort.refresh() once they land. Subscribe to nds:sort:change on the root for every reorder.

Dropmenu Trigger Icon Sync

When sort triggers live inside a .nds-dropmenu, the closed trigger button icon automatically updates to mirror the active sort item's icon after each selection. Icon classes are copied verbatim, so any icon set works without additional configuration.

Usage Guidelines

Best Practices

  • Reach for NDS.Sort directly only when the host widget is neither a Filter nor a Tables. Both of those wire the engine for you, and duplicating wiring causes double-init.
  • Pick direct mode when every sort choice is explicitly labelled (dropmenus, pill groups, radio bars). Include a reset option with an empty data-sort attribute so users can return to the original order.
  • Pick cycle mode when a single trigger carries the state for one key (column headers, inline toggles). Users expect three clicks to return them to where they started.
  • Keep accessor functions pure and cheap. They run once per item per sort, plus once per sample value for type detection. If values need formatting, compute the raw value in the accessor and let NDS.Sort handle comparison.
  • Separate display text from the sortable value when they diverge. A card that reads Free but carries data-sort-price="0" sorts as the cheapest item without the comparator ever seeing the string. Same pattern for N/A, Just now, localized numbers, or any label that would break numeric/date sorting.
  • Override type detection with the types option when sample values are ambiguous (e.g. zip codes, phone numbers, IDs that look like numbers but must sort as strings).
  • Enable urlSync for list pages where users share links or refresh mid-task. Skip it for transient widgets where persistence would feel sticky.
  • Use initialState when the server renders pre-sorted markup. NDS.Sort will honour it without reordering, so aria attributes match the DOM immediately.
  • Hook consumer-specific side effects (pagination refresh, CSS state flags, analytics) through onChange. Avoid listening to nds:sort:change for side effects on the same component: onChange fires synchronously before the event and keeps logic co-located with the create() call.
  • Call sort.destroy() in single-page app teardown. The AbortController drops every click and keydown listener in one step.
  • Note the active state stamp difference by a11y mode: 'pressed' sets data-state="selected" on the active trigger; 'sort' sets data-state="active". CSS rules that key off these stamps must use the correct value for the mode in use.

Options

OptionTypeDefaultPurpose
items selector, NodeList, Array, or function none (required) The elements to reorder. Pass a function to re-resolve on every sort for live item sets.
reorderIn Element items[0].parentElement The parent to re-append items into. Override when items live in a container different from the root.
triggers selector, NodeList, Array, or function none (required) The clickable elements that drive sorting.
accessor (item, key) => value (i, k) => i.getAttribute('data-sort-' + k) Returns the raw sortable value for an item under a given key.
keyFrom (trigger) => key (t) => t.getAttribute('data-sort') || '' Maps a trigger element to its sort key. Return an empty string or null for a reset trigger.
mode 'direct' or 'cycle' 'direct' Trigger behaviour model.
a11y 'pressed', 'sort', or 'none' 'pressed' Which accessibility attribute to write. 'pressed' for toggle-like triggers, 'sort' for table columns, 'none' to suppress.
a11yTarget (trigger) => Element (t) => t.closest('th') The element that carries aria-sort when a11y: 'sort'.
types object {} Per-key overrides for auto-detection. Values: 'number', 'date', 'string'.
initialState { key, dir } or null null Seeds state without reordering. Use when HTML is already sorted by the server.
urlSync { keyParam, dirParam } or false false Persists state in the query string. Reads on create, writes on every change, ascending direction is omitted.
onChange ({ key, dir, orderedItems, state }) => void none Called synchronously after every apply, before the nds:sort:change event fires.

Events

EventDetailFires when
nds:sort:change { key, dir, orderedItems, sort } After every reorder, including programmatic apply() and reset(). Bubbles from the root.

JavaScript API

The NDS.Sort API exposes a factory plus pure helpers. Consumers of Filter and Tables rarely need to call it directly: the widget wires itself on page load. Call NDS.Sort.create() only for custom widgets.

NDS.Sort ships in the delegated bundle, which the loader injects after first paint. On a page that already uses Filter, Tables, or any other delegated component, it is loaded for you. For a standalone widget on a page with no other delegated component, await NDS.loadBundle('delegated') before calling create() so it returns the instance synchronously. An early call through the lazy loader stub returns a Promise instead.

// ── Create an instance ─────────────────────────────── // Returns the NDSSort instance; re-creating on the same root returns the existing one. const sort = NDS.Sort.create(rootElement, { items: '.row', // selector, NodeList, Array, or () => NodeList // a selector string is resolved INSIDE rootElement; // pass a getter for items that live outside it reorderIn: myListEl, // optional; defaults to items[0].parentElement triggers: '.sort-btn', // selector, NodeList, Array, or () => NodeList mode: 'direct', // 'direct' | 'cycle' a11y: 'pressed', // 'pressed' | 'sort' | 'none' a11yTarget: (t) => t.closest('th'), // only used when a11y === 'sort' accessor: (item, key) => item.getAttribute('data-sort-' + key), keyFrom: (trigger) => trigger.getAttribute('data-sort') || '', types: { price: 'number', added: 'date' }, initialState: { key: 'added', dir: 'desc' }, // seeds state without reordering urlSync: { keyParam: 'sort', dirParam: 'dir' }, onChange: ({ key, dir, orderedItems, state }) => { // Consumer-specific post-processing (pagination refresh, CSS hooks, analytics) } }); // ── Instance methods ───────────────────────────────── sort.apply('price', 'desc'); // Sort programmatically sort.apply(null, null); // Same as sort.reset() sort.reset(); // Restore the DOM order captured at create() sort.refresh(); // Re-apply the active sort to items added since sort.getState(); // { key, dir } current state sort.destroy(); // Abort every listener bound by this instance // ── Retrieval ──────────────────────────────────────── NDS.Sort.getInstance(rootElement); // Existing instance or null NDS.Sort.getInstance('#mySortRoot'); // Selector also accepted // ── Events ─────────────────────────────────────────── // Fires on the root, bubbles. Use for cross-component coordination // (e.g. analytics, external state stores). document.addEventListener('nds:sort:change', (e) => { const { key, dir, orderedItems, sort } = e.detail; }); // ── Pure helpers ───────────────────────────────────── // Useful when comparing values outside the instance lifecycle (e.g. server-rendered // initial ordering, custom virtualization). NDS.Sort.detectType(['9,375 SAR', '1,200 SAR']); // 'number' NDS.Sort.detectType(['2026-01-01', '2026-02-01']); // 'date' NDS.Sort.parseValue('9,375 SAR', 'number'); // 9375 NDS.Sort.compare('Apple', 'Banana', 'string', 'asc'); // Negative value
Last Modified Date: 06/08/2026 - 02:16 AM
Was this page useful?
60% of users said Yes from 2843 Feedbacks