a11y-sortable-table
v1.0.0
Published
Framework-agnostic sortable table behavior for accessible semantic data tables.
Maintainers
Readme
A11y Sortable Table
Framework-agnostic sortable table behavior for accessible semantic data tables.
It keeps native table markup intact, applies aria-sort to header cells, preserves focus after sorting, and announces sort changes through a polite live region.
Installation
npm install a11y-sortable-table
pnpm add a11y-sortable-table
yarn add a11y-sortable-tableUsage
import { createSortableTable } from "a11y-sortable-table";
import "a11y-sortable-table/styles.css";
const table = document.querySelector("[data-a11y-sortable-table]");
if (table instanceof HTMLTableElement) {
createSortableTable(table, {
defaultSort: { key: "amount", direction: "descending" },
allowUnsort: true
});
}To initialize a page or fragment:
import { initSortableTables } from "a11y-sortable-table";
initSortableTables();CSS
Default CSS is included for readable table layout, visible focus states, responsive scroll wrappers, and sort indicators.
import "a11y-sortable-table/styles.css";Public custom properties use the --a11y-sortable-table-* and --a11y-table-wrapper-* prefixes.
HTML Structure
Use a real <table> with a <thead> and <tbody>. Give the table an accessible name with a <caption>, aria-label, or aria-labelledby. Sortable headers can provide their own real button with data-sort-key, or expose data-sort-key on the <th> so the plugin can generate the button as progressive enhancement.
<table class="a11y-sortable-table" data-a11y-sortable-table>
<caption>Invoices</caption>
<thead>
<tr>
<th scope="col">
<button class="a11y-sortable-table__button" type="button" data-sort-key="invoice">
Invoice
<span class="a11y-sortable-table__icon" aria-hidden="true"></span>
</button>
</th>
<th scope="col" data-sort-key="amount" data-sort-type="number" data-align="end">
Amount
</th>
</tr>
</thead>
<tbody>
<tr>
<td>INV-1001</td>
<td data-sort-value="199.99" data-align="end">$199.99</td>
</tr>
</tbody>
</table>Use data-sort-type="number" or data-sort-type="date" when text comparison is not appropriate. It can live on either the sort button or the <th>; button values win when both are present. Use data-sort-value on cells when the visible value is formatted for humans.
API
createSortableTable(table, options)
Initializes one table and returns a SortableTableInstance.
createSortableTable(table, {
defaultSort: { key: "date", direction: "descending" },
allowUnsort: true
});initSortableTables(options, root)
Initializes every table[data-a11y-sortable-table] in root or document.
Instance Methods
sortBy(key, direction, config)sorts a column. Use"none"to restore original order.clearSort(config)clears the active sort when one exists.refresh()recollects rows and headers after table markup changes.getSortState()returns{ key, direction, columnIndex }.destroy()removes listeners, generated state, and created live regions.
Options
| Option | Default | Description |
| --- | --- | --- |
| headerSelector | "thead th" | Selector for header cells that are sortable or may contain sortable buttons. |
| buttonSelector | "button[data-sort-key]" | Selector for author-provided sort buttons inside header cells. |
| bodySelector | "tbody" | Selector for the table body whose rows should be sorted. |
| statusElement | null | Existing element or selector used for sort announcements. A polite live region is created when omitted. |
| defaultSort | null | Sorts a key on initialization when sortOnInit is true. |
| initialDirection | "ascending" | Direction used for text columns on first activation. |
| descendingFirstTypes | ['number', 'date'] | Columns with these types sort descending on first activation. |
| locale | document or browser language | Locale passed to string comparison. |
| numeric | true | Enables numeric-aware text comparison. |
| caseFirst | "false" | Controls Intl.Collator case ordering. |
| emptyValuesLast | true | Keeps empty values at the end of sorted rows. |
| announce | true | Enables polite sort announcements. |
| sortOnInit | true | Applies defaultSort during initialization. |
| preserveOriginalOrder | true | Stores original row order so cleared sorts can restore it. |
| allowUnsort | false | Adds a third activation state that restores the original row order and clears aria-sort. |
| customParsers | {} | Maps a column key to a parser function for custom sort values. |
| customSorters | {} | Maps a column key to a row comparator function. |
| labels | built in English labels | Overrides announcement and button labels for localization. |
Options can also be provided with safe data-* attributes such as data-a11y-sortable-table-default-sort, data-a11y-sortable-table-default-direction, and data-a11y-sortable-table-allow-unsort.
Optional URL Hash State
URL hash synchronization is available as an opt-in subpath so the core table behavior never changes browser history by default. Import syncSortableTableHashState from a11y-sortable-table/hash-state after creating a table instance.
import { createSortableTable } from "a11y-sortable-table";
import { syncSortableTableHashState } from "a11y-sortable-table/hash-state";
const table = document.querySelector("#invoices-table");
if (table instanceof HTMLTableElement) {
const instance = createSortableTable(table, { allowUnsort: true });
const cleanupHashState = syncSortableTableHashState(instance, {
table,
namespace: "invoices",
paramName: "sort",
replace: true
});
// Later, when tearing down the table:
cleanupHashState();
instance.destroy();
}The helper reads hashes such as #sort=invoices:amount:descending, applies valid key/direction pairs without announcements or focus changes, updates the hash after a11y-sortable-table:sort, and listens for hashchange so browser history navigation or manual hash edits re-apply state. Use namespace when more than one sortable table can appear on the page. The helper guards browser globals and returns a cleanup function that removes its listeners.
Optional URL Hash Filter State Integration
Row filtering is intentionally not part of the sortable-table core. If URL hash state should control row visibility, use the optional filter-state integration (or copy the pattern into your application) so src/index.ts remains focused on sorting behavior.
import { createSortableTable } from "a11y-sortable-table";
import { syncSortableTableFilterState } from "a11y-sortable-table/filter-state";
const table = document.querySelector("#invoices-table");
const filterStatus = document.querySelector("#invoice-filter-status");
if (table instanceof HTMLTableElement) {
const instance = createSortableTable(table);
const cleanupFilterState = syncSortableTableFilterState(instance, {
table,
statusElement: filterStatus,
filterPrefix: "filter-"
});
// Later, when tearing down the table:
cleanupFilterState();
instance.destroy();
}The integration reads hash parameters such as #filter-status=paid&filter-category=hardware. Rows can expose filter fields with clear data attributes on the row, such as data-filter-status or data-filter-category, or with per-cell values such as data-filter-key="status" data-filter-value="paid". Matching rows remain visible; non-matching rows receive the hidden attribute. Result-count announcements belong to this filtering integration, not the sortable core, and are sent through the optional statusElement live region.
When filtering is combined with sorting, the integration applies filters first and then reapplies the current sort without sort announcements or focus movement so displayed results stay ordered. Treat this as an integration pattern rather than baseline sortable-table behavior.
Optional Current View Summary
Use syncSortableTableSummaryState when users need one clear status sentence for the current table view. It listens for sort, filter, column-visibility, and refresh events, then writes text such as "Sorted by Amount, descending. Showing 2 of 8 rows." to a provided status element.
import { createSortableTable } from "a11y-sortable-table";
import { syncSortableTableSummaryState } from "a11y-sortable-table/summary-state";
const table = document.querySelector("#invoices-table");
const summaryStatus = document.querySelector("#invoice-summary");
if (table instanceof HTMLTableElement && summaryStatus instanceof HTMLElement) {
const instance = createSortableTable(table);
const cleanupSummary = syncSortableTableSummaryState(instance, {
table,
statusElement: summaryStatus
});
// Later, when tearing down the table:
cleanupSummary();
instance.destroy();
}The summary status element is normalized to a polite, atomic live region while the helper is active. Cleanup removes listeners and restores the element's original live-region attributes and text.
Optional Reset State
Use createSortableTableReset to give users a clear recovery action. The helper can wire a reset button, clear active sorting, remove sort and filter-* hash parameters, reveal hidden rows, announce the result, and dispatch a11y-sortable-table:reset for other optional helpers.
import { createSortableTable } from "a11y-sortable-table";
import { createSortableTableReset } from "a11y-sortable-table/reset-state";
const table = document.querySelector("#invoices-table");
const resetButton = document.querySelector("#reset-invoices");
if (table instanceof HTMLTableElement && resetButton instanceof HTMLElement) {
const instance = createSortableTable(table, { allowUnsort: true });
const resetController = createSortableTableReset(instance, {
table,
trigger: resetButton,
statusElement: "#invoice-reset-status",
filterPrefix: "filter-",
sortParamName: "sort"
});
// You can also call resetController.reset() yourself.
// Later, when tearing down the table:
resetController.destroy();
instance.destroy();
}Optional Column Visibility
Use createSortableTableColumnVisibility when users should be able to hide nonessential columns while the table remains semantic. Controls are ordinary checkboxes with data-a11y-sortable-table-column; values match data-sort-key, data-column-key, or generated column keys such as column-1.
<fieldset id="invoice-columns">
<legend>Columns</legend>
<label><input type="checkbox" data-a11y-sortable-table-column="invoice" checked /> Invoice</label>
<label><input type="checkbox" data-a11y-sortable-table-column="amount" checked /> Amount</label>
</fieldset>import { createSortableTable } from "a11y-sortable-table";
import { createSortableTableColumnVisibility } from "a11y-sortable-table/column-visibility";
const table = document.querySelector("#invoices-table");
if (table instanceof HTMLTableElement) {
const instance = createSortableTable(table);
const columns = createSortableTableColumnVisibility(instance, {
table,
controls: "#invoice-columns",
statusElement: "#invoice-column-status",
hiddenColumns: ["internal-notes"]
});
columns.setColumnVisible("amount", false);
columns.reset();
// Later, when tearing down the table:
columns.destroy();
instance.destroy();
}The helper uses the hidden attribute on matching header/body/footer cells, prevents hiding every column by default, updates checkbox state, and restores original cell/control state during destroy().
Optional Saved View
Use syncSortableTableSavedView to persist the last used sort after explicit opt-in. It restores valid stored sort state without announcements or focus movement, then saves later sort changes. Use storage: "session" for tab-scoped state or omit it for localStorage; a custom Storage object can also be passed.
import { createSortableTable } from "a11y-sortable-table";
import { syncSortableTableSavedView } from "a11y-sortable-table/saved-view";
const table = document.querySelector("#invoices-table");
if (table instanceof HTMLTableElement) {
const instance = createSortableTable(table, { allowUnsort: true });
const cleanupSavedView = syncSortableTableSavedView(instance, {
table,
storageKey: "invoices-table:last-sort",
storage: "session"
});
// Later, when tearing down the table:
cleanupSavedView();
instance.destroy();
}Stored values contain only the sort key and direction. The helper clears stored sort state when sorting is cleared or when a11y-sortable-table:reset is dispatched, unless configured otherwise. Document the chosen storage key in your app because persistence can reveal a user's last table view on shared devices.
Scroll Wrappers
Use enhanceScrollableTableWrapper(wrapper) when a table is inside a horizontally scrollable container. The helper only adds tabindex="0", role="region", and an accessible name while horizontal scrolling is needed.
import { enhanceScrollableTableWrapper } from "a11y-sortable-table";
const wrapper = document.querySelector(".a11y-table-wrapper");
if (wrapper instanceof HTMLElement) {
const cleanup = enhanceScrollableTableWrapper(wrapper);
cleanup?.();
}Accessibility Notes
- Sorting controls are native buttons, so Enter and Space work through browser button behavior.
- Header cells with
data-sort-keyare enhanced with generated native buttons when no author-provided sort button exists. - Missing sort button
typeattributes are normalized totype="button"to avoid accidental form submission. - The active column header receives
aria-sort; inactive sortable headers receivearia-sort="none". - Sorting preserves focus on the activated header button.
- Sort changes are announced through a polite live region.
- Optional summary, reset, filter, and column-visibility helpers announce through author-provided polite live regions.
- Optional column visibility hides whole table columns with the
hiddenattribute and prevents hiding every column by default. - The plugin does not change table semantics or require ARIA grid roles.
- Optional saved view persistence is opt-in and stores only sort key and direction.
- The scroll-wrapper helper adds region semantics only when horizontal overflow exists.
- Default motion is subtle and respects
prefers-reduced-motion.
Events
Lifecycle events bubble from the table and include detail.instance.
a11y-sortable-table:inita11y-sortable-table:sorta11y-sortable-table:refresha11y-sortable-table:destroy
Optional helper events also bubble from the table when those helpers are imported.
a11y-sortable-table:filtera11y-sortable-table:summarya11y-sortable-table:reseta11y-sortable-table:columnsa11y-sortable-table:saved-view
Examples
See the GitHub Pages examples:
docs/examples/basic- minimal copyable setup.docs/examples/data-attributes- configure default sort, unsort, descending-first types, and empty values from HTML.docs/examples/custom-order- use custom parsers, custom sorters, custom labels, and a status element.docs/examples/url-state-filters- combine sortable hash state with hash-driven row filters.docs/examples/current-view-summary- summarize current sort and visible row counts withsummary-state.docs/examples/reset-state-recovery- clear sort, filters, hidden rows, and status text withreset-state.docs/examples/column-visibility- hide and restore optional columns with checkbox controls.docs/examples/saved-sort-view- restore the last used sort withsaved-viewandsessionStorage.
Build first so the committed Pages examples can import from docs/dist.
npm run pages:buildGitHub Pages
Generate the branch-published Pages folder with:
npm run pages:buildUse GitHub Pages with Deploy from a branch, the main branch, and the /docs folder. The root dist/ folder stays package-only output; docs/dist/ is generated for the Pages demo. Keep publishable HTML in docs/ so duplicate root demo files do not drift.
Docs Metadata
import { docs } from "a11y-sortable-table/docs";