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

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

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

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.getState(), and sort.destroy() on the instance returned from create(). Subscribe to nds:sort:change on the root for every reorder.

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.

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.

// ── Create an instance ─────────────────────────────── // Returns the NDSSort instance; re-creating on the same root returns the existing one. const sort = NDS.Sort.create(rootElement, { items: '#myList > .row', // selector, NodeList, Array, or () => NodeList 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.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
Was this page useful?
60% of users said Yes from 2843 Feedbacks