Table State
Sorting
Sorting is a list of rules in the state, applied by the engine with a locale-aware comparator. The header button cycles a column; Shift adds it to the list; the position in the list is its precedence.
- Fixture
- Benchmark
On this page
The default comparator handles numbers, dates and text: numbers by value, dates by time, text
through an Intl.Collator built with the locale prop and numeric
collation, so "item 10" sorts after "item 9". Empty values (null, undefined, an empty string or NaN) sort last in both directions. A
column supplies compare when values have a domain order, and the direction is applied
to its result.
Because sorting is state, it can be set before the first render, persisted, or driven from
outside the table. A sort change resets the page index so the user is not left on an empty page.
The sorted header carries aria-sort; with more than one rule, the precedence number
is exposed to assistive technology after the sortOrder message.
Single and multi-column sort
Single and multi-column sorting with visible priority and controlled state.
Joined | ||||||
|---|---|---|---|---|---|---|
Noah Brenner | Active | Design | Asia | $44,096 | 85% | 2026-08-06 |
Lucas Ferreira | Invited | Growth | Asia | $43,610 | 82% | 2026-02-16 |
Selin Aydın | Active | Engineering | Americas | $36,177 | 72% | 2026-03-23 |
İpek Yılmaz | Away | Operations | Americas | $35,691 | 69% | 2026-06-05 |
Rafael Duarte | Invited | Growth | Europe | $28,258 | 59% | 2026-07-12 |
Ömer Say | Active | Design | Europe | $27,772 | 56% | 2026-01-22 |
Mei Tanaka | Away | Operations | Asia | $20,339 | 46% | 2026-02-01 |
Hana Kowalska | Active | Engineering | Asia | $19,853 | 43% | 2026-05-11 |
Jonas Lindqvist | Active | Design | Americas | $12,420 | 33% | 2026-06-18 |
Elias Moreau | Invited | Growth | Americas | $11,934 | 30% | 2026-09-28 |
Amara Okafor | Active | Engineering | Europe | $4,501 | 20% | 2026-01-07 |
Priya Raman | Away | Operations | Europe | $4,015 | 98% | 2026-04-17 |
[{"id":"revenue","desc":true}] <script lang="ts">
import { DataTable, defaultState, type Column, type TableState } from '@svelte-lean/table';
import { memberColumns } from '$lib/demo/columns';
import { people, type Person } from '$lib/demo/data';
const data = people(12);
const ORDER = ['Active', 'Away', 'Invited'];
const columns: Column<Person>[] = memberColumns().map((column) =>
column.id === 'status'
? { ...column, compare: (a, b) => ORDER.indexOf(String(a)) - ORDER.indexOf(String(b)) }
: column.id === 'joined'
? { ...column, sortable: false }
: column
);
let tableState = $state<Partial<TableState>>(defaultState({ sorting: [{ id: 'revenue', desc: true }] }));
</script>
<!-- Click a header to cycle none, ascending, descending; Shift+click adds a second column. -->
<DataTable {data} {columns} getRowId={(row) => row.id} bind:state={tableState} pagination={false} label="Members" />
<pre>{JSON.stringify(tableState.sorting)}</pre>- Shift+click adds or cycles a column without clearing the others; a plain click replaces the list.
- In manual mode the engine applies no sorting; the state is sent to the server as it is.
- With
groupBy(), rows stay sorted inside each group and groups appear in the order of their first sorted row.
Custom sort controls
A header can host its own sort control. headerCell receives a HeaderContext: the column, its current sort, the whole sorting list, toggleSort() (the column's own cycle) and setSorting(), which replaces the list with any sort ids and resets the page. With sortable: false the header renders no sort button, so the snippet's controls are
not nested in one. sortKeys lists the further ids that count as this column's sort,
so aria-sort and the context's sort follow whichever of them is active.
Activity | |||
|---|---|---|---|
Amara Okafor | Engineering | $4,501 | 2026-01-07 · 20% |
Jonas Lindqvist | Design | $12,420 | 2026-06-18 · 33% |
Mei Tanaka | Operations | $20,339 | 2026-02-01 · 46% |
Rafael Duarte | Growth | $28,258 | 2026-07-12 · 59% |
Selin Aydın | Engineering | $36,177 | 2026-03-23 · 72% |
Noah Brenner | Design | $44,096 | 2026-08-06 · 85% |
Priya Raman | Operations | $4,015 | 2026-04-17 · 98% |
Elias Moreau | Growth | $11,934 | 2026-09-28 · 30% |
Hana Kowalska | Engineering | $19,853 | 2026-05-11 · 43% |
Ömer Say | Design | $27,772 | 2026-01-22 · 56% |
İpek Yılmaz | Operations | $35,691 | 2026-06-05 · 69% |
Lucas Ferreira | Growth | $43,610 | 2026-02-16 · 82% |
[]
<script lang="ts">
import { DataTable, type Column, type HeaderContext, type TableState } from '@svelte-lean/table';
import { memberColumns, pick } from '$lib/demo/columns';
import { people, type Person } from '$lib/demo/data';
const data = people(12);
const FIELDS = [
{ id: 'joined', label: 'Joined date' },
{ id: 'progress', label: 'Progress' }
];
const columns: Column<Person>[] = [
...pick(memberColumns(), ['name', 'team']),
{ ...memberColumns().find((c) => c.id === 'revenue')!, sortDescFirst: true },
{
id: 'activity',
header: 'Activity',
accessor: (row) => `${row.joined} · ${row.progress}%`,
width: 190,
sortable: false,
sortKeys: FIELDS.map((field) => field.id),
headerCell: activity
},
// Hidden columns give the client row model values to sort the two keys by.
{ id: 'joined', header: 'Joined' },
{ id: 'progress', header: 'Progress' }
];
let tableState = $state<Partial<TableState>>({ hidden: ['joined', 'progress'] });
</script>
{#snippet activity({ column, sort, setSorting }: HeaderContext<Person>)}
<span class="head">
{column.header}
<button
type="button"
class="trigger"
popovertarget="activity-sort"
aria-label="Sort {column.header}"
data-active={sort ? '' : undefined}
>
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"
><path d="M2.5 4h11M4.5 8h7M6.5 12h3" /></svg
>
</button>
</span>
<div id="activity-sort" popover data-slean="popover" data-slean-side="bottom" data-slean-align="end">
<label>
Order by
<select
value={sort?.id ?? ''}
onchange={(e) =>
setSorting(
e.currentTarget.value ? [{ id: e.currentTarget.value, desc: sort?.desc ?? true }] : []
)}
>
<option value="">Unsorted</option>
{#each FIELDS as field (field.id)}<option value={field.id}>{field.label}</option>{/each}
</select>
</label>
<label>
Direction
<select
value={sort?.desc === false ? 'asc' : 'desc'}
disabled={!sort}
onchange={(e) =>
sort && setSorting([{ id: sort.id, desc: e.currentTarget.value === 'desc' }])}
>
<option value="desc">Newest or highest first</option>
<option value="asc">Oldest or lowest first</option>
</select>
</label>
</div>
{/snippet}
<DataTable
{data}
{columns}
getRowId={(row) => row.id}
bind:state={tableState}
pagination={false}
label="Members: custom sort control"
/>
<pre class="state" aria-label="Sort state">{JSON.stringify(tableState.sorting ?? [])}</pre>sortDescFirstsuits amounts, dates and counts, where the largest or newest value is the one users look for first.- A sort id that names no column is kept in the state and ignored by the client row model; declare a hidden column with that id to sort by it on the client.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | in the header | Moves between the sort buttons and resize handles. |
| Enter/Space | on a sort button | Cycles the column: ascending, descending, none (descending first with sortDescFirst). |
| Shift+Enter/Shift+Space | on a sort button | Cycles the column while keeping the other sorted columns. |
Engine
The renderer calls table.toggleSort(id, multi); the pure function underneath is
exported for application code that builds its own controls.
import { toggleSort } from '@svelte-lean/table';
// none → ascending → descending → none; multi keeps the other rules.
toggleSort([], 'revenue'); // [{ id: 'revenue', desc: false }]
toggleSort([{ id: 'revenue', desc: false }], 'revenue'); // [{ id: 'revenue', desc: true }]
toggleSort([{ id: 'revenue', desc: true }], 'revenue'); // []
toggleSort([{ id: 'team', desc: false }], 'revenue', true); // [{ id: 'team', desc: false }, { id: 'revenue', desc: false }]
// descFirst (a column with sortDescFirst): none → descending → ascending → none.
toggleSort([], 'revenue', false, true); // [{ id: 'revenue', desc: true }]API
| Name | Of | Type | Default | Description |
|---|---|---|---|---|
sorting | TableState | Sort[] | [] | Ordered sort rules; the array order is the multi-sort precedence. |
sortable | Sort, filter and aggregate | boolean | true | Header sorting; empty values sort last in both directions. |
compare | Sort, filter and aggregate | (a: unknown, b: unknown, aRow: T, bRow: T) => number | numbers, dates, then a locale collator | Custom comparator; the sort direction is applied to its result. |
sortDescFirst | Sort, filter and aggregate | boolean | false | The first activation sorts descending: descending, ascending, none. |
sortKeys | Sort, filter and aggregate | string[] | undefined | Further sort ids that count as this column sorted (aria-sort, the icon), for a headerCell that sorts by several fields. |
headerCell | Data and content | Snippet<[HeaderContext<T>]> | undefined | Custom leaf header content. Receives the column, its sort, the sort list, toggleSort() and setSorting(); with sortable: false it can host a sort control of its own. |
HeaderContext<T> | Row model | { column; sort: { id; desc; index } | undefined; sorting: Sort[]; toggleSort(multi?); setSorting(sorting) } | – | Argument of headerCell snippets. |
locale | Presentation | string | undefined | Locale of the collator used for sorting and of case folding in search and filters. |
manual | Controlled state | boolean | false | Treats data as already sorted, filtered and paged by the application; the client model applies none of them. |
sortOrder | TableMessages | string | 'Sort order' | Read to assistive technology before the multi-sort precedence number. |
toggleSort | Root entry | (sorting: Sort[], id: string, multi?: boolean, descFirst?: boolean) => Sort[] | – | Pure sort cycle none, ascending, descending, none; descFirst starts descending. |
toggleSort | Table | (id: string, multi?: boolean) => void | – | Cycles none, ascending, descending, none (descending first with sortDescFirst); multi keeps other sorted columns. Resets the page. |
sortOf | Table | (column: string | Column<T>) => { id: string; desc: boolean; index: number } | undefined | – | Sort rule, direction and precedence of a column; given a column, a rule on one of its sortKeys counts. |
Sort | Events and positions | { id: string; desc: boolean } | – | One sort rule. |