npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

a11y-sortable-table

v1.0.0

Published

Framework-agnostic sortable table behavior for accessible semantic data tables.

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-table

Usage

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-key are enhanced with generated native buttons when no author-provided sort button exists.
  • Missing sort button type attributes are normalized to type="button" to avoid accidental form submission.
  • The active column header receives aria-sort; inactive sortable headers receive aria-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 hidden attribute 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:init
  • a11y-sortable-table:sort
  • a11y-sortable-table:refresh
  • a11y-sortable-table:destroy

Optional helper events also bubble from the table when those helpers are imported.

  • a11y-sortable-table:filter
  • a11y-sortable-table:summary
  • a11y-sortable-table:reset
  • a11y-sortable-table:columns
  • a11y-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 with summary-state.
  • docs/examples/reset-state-recovery - clear sort, filters, hidden rows, and status text with reset-state.
  • docs/examples/column-visibility - hide and restore optional columns with checkbox controls.
  • docs/examples/saved-sort-view - restore the last used sort with saved-view and sessionStorage.

Build first so the committed Pages examples can import from docs/dist.

npm run pages:build

GitHub Pages

Generate the branch-published Pages folder with:

npm run pages:build

Use 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";