@mshafiqyajid/react-table
Headless table hook and styled component. Sort, filter, paginate, select rows. 0.4.0 adds multi-column sort, column visibility menu, CSV / JSON export helpers, single / multi / range selection, and an opt-in ARIA grid keyboard model. All earlier APIs (accessor + custom sort/filter, footer + aggregates, server-side modes, page-size selector, density toggle, render slots, highlight, storageKey, column resize, expandable rows) still apply.
Playground #
| Name | Role | Status | Joined | Salary |
|---|---|---|---|---|
| Alice Johnson | Engineer | Active | 2022-01 | $92k |
| Bob Chen | Designer | Active | 2021-06 | $78k |
| Carol Williams | Manager | On leave | 2020-03 | $115k |
| David Kim | Engineer | Active | 2023-02 | $86k |
| Total | 703000 |
import { TableStyled } from "@mshafiqyajid/react-table/styled";
import "@mshafiqyajid/react-table/styles.css";
<TableStyled
data={DATA}
columns={COLUMNS}
showFooter
showDensityToggle
showColumnMenu
exportable
highlightMatches
/>Install #
npm install @mshafiqyajid/react-table Quick start #
import { TableStyled } from "@mshafiqyajid/react-table/styled";
import type { ColumnDef } from "@mshafiqyajid/react-table";
import "@mshafiqyajid/react-table/styles.css";
type User = { id: number; name: string; email: string };
const columns: ColumnDef<User>[] = [
{ key: "name", header: "Name", sortable: true },
{ key: "email", header: "Email", filterable: true },
];
<TableStyled data={users} columns={columns} sortable filterable paginate pageSize={10} hoverable /> Expandable rows #
<TableStyled
data={users}
columns={cols}
expandable={{
renderExpanded: (user) => (
<div>
<strong>{user.name}</strong>
<p>{user.bio}</p>
</div>
),
}}
defaultExpandedRowIds={["3"]}
/> Adds a chevron column; clicking toggles a detail row beneath. Pair with defaultExpandedRowIds (uncontrolled) or expandedRowIds + onExpandedRowsChange (controlled).
What's new in 0.4.0 #
Multi-column sort. Pass multiSort and shift-click (or Cmd/Ctrl-click) on a header to append a sort entry. The hook returns sorts: SortBy[]; existing sortKey / sortDir still mirror the head entry.
Column visibility. ColumnDef.hidden and defaultHidden, plus a controlled columnVisibility map. Set showColumnMenu to render a "Columns" button + checklist popover in the toolbar.
CSV / JSON export. exportTableCSV / exportTableJSON ship as pure helpers and skip hidden columns. The styled exportable prop wires buttons that export the current filter+sort.
Selectable variants. selectable now accepts "single" | "multi" | "range" (true stays "multi"). Range mode supports shift-click between an anchor row and a target row.
Keyboard nav + ARIA grid. Opt in with ariaGrid for role="grid", row / col indexes, and arrow / Home / End / PageUp / PageDown navigation. Pair with ariaLabel or ariaLabelledBy.
Multi-column sort #
<TableStyled
data={users}
columns={cols}
multiSort
defaultSort={[
{ key: "dept", dir: "asc" },
{ key: "salary", dir: "desc" },
]}
onSortChange={(sorts) => console.log(sorts)}
/> CSV / JSON export #
import { exportTableCSV } from "@mshafiqyajid/react-table";
const csv = exportTableCSV({ rows, columns, filename: "users.csv", download: true });
// Or via the styled toolbar β exports the current filter+sort:
<TableStyled data={users} columns={cols} exportable={["csv", "json"]} /> Column resize #
const cols: ColumnDef<User>[] = [
{ key: "name", header: "Name", resizable: true, minWidth: 120 },
{ key: "email", header: "Email", resizable: true },
]; Drag the right edge of any header to resize. minWidth / maxWidth clamp the range (defaults 60 / 800 px). Pointer-event-driven, no external dep.
Headless #
import { useTable } from "@mshafiqyajid/react-table";
const { rows, sortKey, sortDir, toggleSort, page, pageCount, setPage } = useTable({
data: users,
columns,
pageSize: 10,
});
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} onClick={() => toggleSort(col.key)}>
{col.header} {sortKey === col.key ? (sortDir === "asc" ? "β²" : "βΌ") : ""}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}><td>{row.name}</td><td>{row.email}</td></tr>
))}
</tbody>
</table>
); TableStyled props #
| Prop | Type | Default | Description |
|---|---|---|---|
| data / columns | T[] / ColumnDef<T>[] | β | Required |
| pageSize | number | 10 | Rows per page (controlled when onPageSizeChange is provided) |
| pageSizeOptions | number[] | β | Show a row-size selector with these options |
| showPageSize | boolean | true when options provided | Force show/hide the selector |
| onPageSizeChange | (size) => void | β | Make pageSize controlled |
| showFooter / stickyFooter | boolean | false | Render <tfoot> from ColumnDef.footer + aggregate |
| showDensityToggle | boolean | false | SM/MD/LG button group in toolbar |
| onSizeChange | (size) => void | β | Make size controlled (use with density toggle) |
| highlightMatches | boolean | false | Wrap matching substrings in <mark>. Skipped for cells with custom render. |
| renderEmpty / renderLoading / renderError | () => ReactNode | β | Replace the default state row |
| error / errorText / onRetry | boolean / ReactNode / fn | β | Error state and retry hook (passed into renderError) |
| getRowProps | (row, i) => partial <tr> props | β | Per-row className / data-* / onClick override |
| getCellProps | (row, col) => partial <td> props | β | Per-cell escape hatch |
| manualSorting / manualFiltering / manualPagination | boolean | false | Skip in-memory ops; consumer handles them |
| totalCount | number | β | Drives pageCount under manualPagination |
| storageKey / storage | string / Storage | β | Persist sort/filter/page across reloads |
| size / tone | "sm" | "md" | "lg" / "neutral" | "primary" | "md" / "neutral" | Row density and accent |
| striped / bordered / hoverable / stickyHeader | boolean | β | Style toggles |
| loading / emptyText / caption | boolean / ReactNode / string | β | Existing behavior |
| multiSort | boolean | false | Allow more than one sort column at once. Shift / Cmd / Ctrl-click on a header appends. |
| onSortChange | (sorts: SortBy[]) => void | β | Fires whenever the sorts array changes |
| showColumnMenu | boolean | false | Toolbar "Columns" button + checklist popover |
| columnVisibility / onColumnVisibilityChange | Record<string,bool> / fn | β | Controlled visibility map |
| exportable | boolean | ("csv" | "json")[] | β | Render export button(s) in the toolbar |
| exportFilename | string | "table" | Filename without extension |
| selectable | boolean | "single" | "multi" | "range" | false | true continues to mean multi (no break) |
| selectedIds / onSelectChange | string[] / (ids) => void | β | Controlled selection |
| ariaGrid | boolean | false | role="grid" + aria-rowindex / aria-colindex + arrow nav |
| ariaLabel / ariaLabelledBy | string | β | Accessible name for the grid |
| onSelect / onRowClick / rowKey / toolbar / page / onPageChange | β | β | Existing behavior |
ColumnDef<T> fields #
| Field | Type | Default | Description |
|---|---|---|---|
| key / header | string | β | Required |
| accessor | (row) => unknown | β | For nested or computed values; falls back to row[key] |
| sortable / filterable | boolean | false | Per-column flags |
| sortFn | (a, b, dir) => number | β | Custom comparator |
| filterFn | (row, query) => boolean | β | Custom predicate |
| aggregate | "sum" | "avg" | "min" | "max" | "count" | (rows) => unknown | β | Computes against the filtered rows; surfaced in result.aggregates |
| footer | ReactNode | (rows, aggregate) => ReactNode | β | Footer cell content (requires showFooter) |
| hidden | boolean | false | Always hide the column (not toggleable through the menu) |
| defaultHidden | boolean | false | Initial visibility for the column-visibility menu |
| align / sticky / width / render | β | β | Existing behavior |
Hook example: server-side mode #
const { rows } = useTable({
data, // current page from your API
columns,
manualSorting: true,
manualFiltering: true,
manualPagination: true,
totalCount: 4218, // total available rows; drives pageCount
storageKey: "users-table",
onSort: (key, dir) => fetch(`/api/users?sort=${key}.${dir}`),
});