TanStack Table is a genuinely great headless library — for virtualised, grouped, pivoted, server-paginated grids. Most admin pages need a table that sorts when you click a header and lets you tick rows. Reaching for the full abstraction there is renting a crane to hang a photo.
Velocity X's standard-sortable-data-table is the ~200-line version: sortable columns, row selection, sticky header, empty state. One file, readable in a sitting, restyled with your Tailwind config because it is your file.
The Money Pattern
The design decision that makes a table maintainable: columns are data, not JSX. The table maps over a definition array, and sorting is a property of the column:
type Column<T> = {
key: string;
label: string;
sort?: (a: T, b: T) => number;
render?: (row: T) => ReactNode;
};
const columns: Column<User>[] = [
{ key: 'name', label: 'Name', sort: (a, b) => a.name.localeCompare(b.name) },
{ key: 'mrr', label: 'MRR', sort: (a, b) => a.mrr - b.mrr,
render: (u) => formatCurrency(u.mrr) },
{ key: 'actions', label: '', render: (u) => <RowMenu id={u.id} /> },
];
Click handling is then trivial: same column toggles direction, new column resets to ascending, and the sorted array is a useMemo over [rows, sortKey, dir]. No reducer, no table "instance", no plugin system.
The Catch
Two details sort (sorry) the professionals from the demos. First, aria-sort on the active header — "ascending" / "descending", removed from the others. Without it the sort state is invisible to assistive tech, and it's one attribute.
Second, select-all is a three-state checkbox: none, some (indeterminate), all. The indeterminate property can only be set via a ref — it's not an attribute — which is exactly the kind of thing you want already handled in a file you own rather than rediscovered at 6pm.
And know when you've outgrown it: 10k+ rows needs virtualisation, server pagination needs server sort. That's when TanStack earns its abstraction — upgrade then, not pre-emptively.
The Verdict
Columns as data, comparator per column, aria-sort, three-state select-all — that's the entire table. The finished file lives in Velocity X's Standard Data category beside the editable, grouped and tree-table variants when the requirements grow teeth.









