react ~13 KB 0 deps v0.5.0 β†— GitHub β†—

@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 #

NameRoleStatusJoinedSalary
Alice JohnsonEngineerActive2022-01$92k
Bob ChenDesignerActive2021-06$78k
Carol WilliamsManagerOn leave2020-03$115k
David KimEngineerActive2023-02$86k
1 / 2
Props
TSX
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 #

PropTypeDefaultDescription
data / columnsT[] / ColumnDef<T>[]β€”Required
pageSizenumber10Rows per page (controlled when onPageSizeChange is provided)
pageSizeOptionsnumber[]β€”Show a row-size selector with these options
showPageSizebooleantrue when options providedForce show/hide the selector
onPageSizeChange(size) => voidβ€”Make pageSize controlled
showFooter / stickyFooterbooleanfalseRender <tfoot> from ColumnDef.footer + aggregate
showDensityTogglebooleanfalseSM/MD/LG button group in toolbar
onSizeChange(size) => voidβ€”Make size controlled (use with density toggle)
highlightMatchesbooleanfalseWrap matching substrings in <mark>. Skipped for cells with custom render.
renderEmpty / renderLoading / renderError() => ReactNodeβ€”Replace the default state row
error / errorText / onRetryboolean / 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 / manualPaginationbooleanfalseSkip in-memory ops; consumer handles them
totalCountnumberβ€”Drives pageCount under manualPagination
storageKey / storagestring / Storageβ€”Persist sort/filter/page across reloads
size / tone"sm" | "md" | "lg" / "neutral" | "primary""md" / "neutral"Row density and accent
striped / bordered / hoverable / stickyHeaderbooleanβ€”Style toggles
loading / emptyText / captionboolean / ReactNode / stringβ€”Existing behavior
multiSortbooleanfalseAllow 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
showColumnMenubooleanfalseToolbar "Columns" button + checklist popover
columnVisibility / onColumnVisibilityChangeRecord<string,bool> / fnβ€”Controlled visibility map
exportableboolean | ("csv" | "json")[]β€”Render export button(s) in the toolbar
exportFilenamestring"table"Filename without extension
selectableboolean | "single" | "multi" | "range"falsetrue continues to mean multi (no break)
selectedIds / onSelectChangestring[] / (ids) => voidβ€”Controlled selection
ariaGridbooleanfalserole="grid" + aria-rowindex / aria-colindex + arrow nav
ariaLabel / ariaLabelledBystringβ€”Accessible name for the grid
onSelect / onRowClick / rowKey / toolbar / page / onPageChangeβ€”β€”Existing behavior

ColumnDef<T> fields #

FieldTypeDefaultDescription
key / headerstringβ€”Required
accessor(row) => unknownβ€”For nested or computed values; falls back to row[key]
sortable / filterablebooleanfalsePer-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
footerReactNode | (rows, aggregate) => ReactNodeβ€”Footer cell content (requires showFooter)
hiddenbooleanfalseAlways hide the column (not toggleable through the menu)
defaultHiddenbooleanfalseInitial 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}`),
});
Edit this page on GitHub