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.
75
300
25
Free
150
1200
// The cards live outside the root that holds the buttons, so `items` is a
// getter rather than a selector string — a string is resolved inside the root.
const items = document.getElementById('sortDirectItems');
NDS.Sort.create(document.getElementById('sortDirectRoot'), {
items: function () { return items.querySelectorAll(':scope > .nds-card'); },
reorderIn: items,
triggers: '[data-sort]',
mode: 'direct',
a11y: 'pressed',
types: { price: 'number' }
});
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.
75
300
25
Free
150
1200
const root = document.getElementById('sortCycleRoot');
// The cards live outside the root that holds the buttons, so `items` is a
// getter rather than a selector string — a string is resolved inside the root.
const items = document.getElementById('sortCycleItems');
NDS.Sort.create(root, {
items: function () { return items.querySelectorAll(':scope > .nds-card'); },
reorderIn: items,
triggers: '.nds-sort-cycle-btn',
mode: 'cycle',
a11y: 'pressed',
keyFrom: (btn) => btn.dataset.sortKey,
types: { price: 'number' },
onChange: (state) => {
// Swap each cycle-button's icon to reflect its current state.
root.querySelectorAll('.nds-sort-cycle-btn').forEach((btn) => {
const icon = btn.querySelector('i');
if (!icon) return;
const isActive = btn.dataset.sortKey === state.key && state.dir;
icon.className = !isActive
? 'nds-icon nds-hgi-sorting-05'
: state.dir === 'asc'
? 'nds-icon nds-hgi-sort-by-up-02'
: 'nds-icon nds-hgi-sort-by-down-02';
});
}
});
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.
|
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
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.
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 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.
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.
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.
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.
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-sortattribute 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.Sorthandle 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
typesoption when sample values are ambiguous (e.g. zip codes, phone numbers, IDs that look like numbers but must sort as strings). - Enable
urlSyncfor list pages where users share links or refresh mid-task. Skip it for transient widgets where persistence would feel sticky. - Use
initialStatewhen 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 tonds:sort:changefor side effects on the same component:onChangefires synchronously before the event and keeps logic co-located with thecreate()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
a11ymode:'pressed'setsdata-state="selected"on the active trigger;'sort'setsdata-state="active". CSS rules that key off these stamps must use the correct value for the mode in use.
Options
| Option | Type | Default | Purpose |
|---|---|---|---|
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
| Event | Detail | Fires 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.