Data table

Column-driven table with page and cursor pagination, sortable headers, a rows-per-page picker, loading skeleton, and empty state.

Preview

Interactive examples use the same @flux/ui build as production. Toggle theme in the header for light and dark. Motion tokens and layout patterns live under Motion and Patterns in the sidebar.

Live preview
Status
Acme Corp₹48,200Active
Globex₹12,750Pending
Initech₹93,400Active
Showing 1–3 of 8 results

Installation

Add the package to your app (full Next.js + Tailwind steps on Installation):

npm install @payglocal_ui/flux-ui

Usage

Import

import { DataTable, Button, type Column } from "@payglocal_ui/flux-ui";

Example

const columns: Column<Row>[] = [
  { key: "name", header: "Name", render: (r) => r.name },
];

// Basic usage — client-side paging at 10 rows a page.
<DataTable rowKey={(r) => r.id} columns={columns} data={rows} />

// ── Pagination ────────────────────────────────────────────────────────
// One `pagination` prop covers every mode, so which one an endpoint needs
// is the only thing that differs between two tables.

// The response carries a row TOTAL: a full numbered strip with ellipses,
// and "Showing 1–15 of 141 results".
<DataTable
  rowKey={(r) => r.id}
  columns={columns}
  data={page.rows}
  pagination={{
    mode: "page",
    page,
    pageSize: 15,
    total: page.totalCount,
    onPageChange: setPage,
  }}
/>

// The response carries NO total (a nextCursor / exclusiveStartKey API).
// "Showing 1–15" with no total and no page count, and the strip numbers
// only the pages a cursor can actually step to: the previous page, this
// one, and — only when hasNext says so — the next. On page 2 that reads
// 1 · 2 · 3, so the next page is visible as a number rather than hiding
// behind an arrow.
<DataTable
  rowKey={(r) => r.id}
  columns={columns}
  data={page.rows}
  pagination={{
    mode: "cursor",
    page,
    pageSize: 15,
    hasNext: !!page.nextCursor,
    onNext: () => goForward(page.nextCursor),
    onPrev: () => goBack(),
  }}
/>

// Rows per page — one prop pair, drawn at the far left of the footer.
// No hand-rolled footer, and no second pager in the same app.
<DataTable
  rowKey={(r) => r.id}
  columns={columns}
  data={rows}
  pagination={{
    mode: "page",
    page,
    pageSize,
    total,
    onPageChange: setPage,
    pageSizeOptions: [15, 25, 50, 100],
    onPageSizeChange: (n) => { setPageSize(n); setPage(1); },
  }}
/>

// No footer at all.
<DataTable rowKey={(r) => r.id} columns={columns} data={rows} pagination={{ mode: "none" }} />

// ── Sorting ───────────────────────────────────────────────────────────
// Modelled on antd's Table: a column opts in with `sorter`, and state is
// reported as { columnKey, order } using antd's "ascend" / "descend".
// Client and server sorting are told apart by the COLUMN, so one grid can
// mix them.
const columns: Column<Row>[] = [
  // The table sorts these rows itself, before paging.
  { key: "name", header: "Name", sorter: (a, b) => a.name.localeCompare(b.name), render: (r) => r.name },

  // `true` = the caller orders the rows (a server query). The table only
  // reports the click. sortDirections starts the cycle at descending,
  // which is the first click money and dates almost always want.
  { key: "amount", header: "Amount", sorter: true, sortDirections: ["descend", "ascend"], render: (r) => r.amount },
];

// Uncontrolled: the table remembers. All a client-sorted grid needs.
<DataTable rowKey={(r) => r.id} columns={columns} data={rows} />

// Controlled: feed the state into the request.
<DataTable
  rowKey={(r) => r.id}
  columns={columns}
  data={rows}
  sorting={{ value: sort, onChange: setSort }}
/>

// Content layout — columns shrink to their intrinsic width; leftover
// space stays empty to the right instead of stretching the columns.
<DataTable
  rowKey={(r) => r.id}
  columns={columns}
  data={rows}
  tableLayout="content"
/>

// Row action — a render-prop button revealed on row hover, pinned to
// the right edge of the viewport. Prefer this over rowCta when you
// need per-row navigation or a custom button style.
<DataTable
  rowKey={(r) => r.id}
  columns={columns}
  data={rows}
  rowAction={(row) => (
    <Button variant="outline" size="sm" onClick={() => openDetails(row)}>
      View details
    </Button>
  )}
/>

// Row click — the whole row is the target, including cell padding and
// the gaps between columns. The row gets cursor-pointer and keyboard
// access (focusable, Enter / Space). Clicks that start inside a button,
// link, form control or anything marked data-row-click-ignore do not
// fire it, so per-row buttons and copy controls keep working on their
// own without stopping propagation.
<DataTable
  rowKey={(r) => r.id}
  columns={columns}
  data={rows}
  onRowClick={(row) => openDetails(row)}
/>