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.
Zakat Payment
75
Passport Renewal
300
Birth Certificate
25
Identity Verification
Free
Driver License
150
Business Registration
1200
NDS.Sort.create(document.getElementById('sortDirectRoot'), {
items: '#sortDirectItems > .nds-card',
reorderIn: document.getElementById('sortDirectItems'),
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.
Zakat Payment
75
Passport Renewal
300
Birth Certificate
25
Identity Verification
Free
Driver License
150
Business Registration
1200
const root = document.getElementById('sortCycleRoot');
NDS.Sort.create(root, {
items: '#sortCycleItems > .nds-card',
reorderIn: document.getElementById('sortCycleItems'),
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.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-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.
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.