Skip to content
WANCRAFT

G.177Data & Feedback

Data Table

A sortable, selectable table on real <table> semantics with multi-sort, resizable columns, density and paging.

NewReactTailwindadvanced

Overview

Data Table is a typed, generic table built on native <table>, <caption>, <th scope> and <col> elements. Column headers are buttons: click or press Enter to sort ascending, descending, then off, and hold Shift to add secondary sorts that show their priority. Rows select with checkboxes, Shift + click selects a range, and the header checkbox is tri-state with a real indeterminate state. Columns resize by dragging their edge or by focusing the handle and using the arrow keys, the header sticks while the body scrolls, rows highlight on hover and tint when selected, and a toggle switches between comfortable and compact density. The body pages or loads more, with loading skeletons and an empty state. Columns are plain objects: { key, header, cell?, sortable?, sortValue?, align?, width? }.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/data-table.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/data-table.json

Method 02 · Copy and paste

  1. 1. No extra dependencies — React and Tailwind CSS only.
  2. 2. Copy the source from the Code tab into components/ui/data-table.tsx.
  3. 3. Import it and adjust props.
Dependencies
None
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { DataTable, type DataTableColumn } from "@/components/ui/data-table";

export function DataTableExample() {
  type Invoice = { id: string; customer: string; amount: number };
  const rows: Invoice[] = [
    { id: "INV-1001", customer: "Northwind Studio", amount: 1840 },
    { id: "INV-1002", customer: "Kampung Labs", amount: 920.5 },
    { id: "INV-1003", customer: "Atlas & Oak", amount: 4310 },
  ];
  const columns: DataTableColumn<Invoice>[] = [
    { key: "id", header: "Invoice", sortable: true, width: 120 },
    { key: "customer", header: "Customer", sortable: true, width: 200 },
    { key: "amount", header: "Amount", sortable: true, align: "right", cell: (r) => `$${r.amount.toFixed(2)}` },
  ];

  return (
    <div className="w-full max-w-3xl">
      <DataTable
        caption="Invoices"
        columns={columns}
        rows={rows}
        getRowId={(r) => r.id}
      />
    </div>
  );
}

03Props

PropTypeDefaultDescription
columnsDataTableColumn<T>[]—Required. { key, header, cell?, sortable?, sortValue?, align?, width?, minWidth? }.
rowsT[]—Required. The data; sorting is stable and keeps empty values last.
getRowId(row: T) => string—Required. Stable id for selection and keys.
captionstring—Required. The table's <caption> (always present for screen readers).
showCaptionbooleantrueAlso show the caption in the toolbar.
selectablebooleantrueCheckbox column with a tri-state select-all header and Shift-click ranges.
selected / defaultSelected / onSelectedChangestring[] / string[] / (ids) => void—Controlled or uncontrolled selected row ids.
sort / defaultSort / onSortChangeSortRule[] / SortRule[] / (sort) => void—Sort rules { key, direction }; the first is primary and sets aria-sort.
multiSortbooleantrueShift + click or Shift + Enter on a header adds a secondary sort.
resizablebooleantrueDrag column edges, or focus a handle and use arrows (Shift = 40px), Home / End, Enter to reset.
pagination"pages" | "load-more""pages"Paged footer or a Load more button.
pageSizenumber10Rows per page / per load.
density / defaultDensity / onDensityChange"comfortable" | "compact"— / "comfortable"Row padding; controlled or uncontrolled.
showDensityTogglebooleantrueShow the density toggle in the toolbar.
stickyHeaderbooleantrueKeep the header visible while the body scrolls.
maxHeightnumber380Scroll area height limit in px; 0 removes it.
loadingbooleanfalseSkeleton rows and aria-busy.
emptyMessageReactNode"No rows to show."Shown when rows is empty.
accentstring"#FF7448"Selection tint, sort arrows, focus rings and resize handles.

04Features

  • 01Click or Enter on a header to sort asc → desc → off; Shift adds secondary sorts with priority badges
  • 02Checkbox selection with a tri-state (indeterminate) select-all and Shift-click range selection
  • 03Column resizing by drag (no re-render per pointer move) or keyboard on a focusable separator
  • 04Sticky header, row hover highlight, selected-row tint and a comfortable / compact density toggle
  • 05Pagination or Load more, loading skeletons and an empty state
  • 06Generic and typed: DataTable<T> with { key, header, cell?, sortable?, align?, width? } columns
input · clickinput · keyboardinput · drag

05Accessibility

  • Real <table> semantics: <caption>, <th scope="col">, <colgroup>; the scroll area is a focusable, labelled region so keyboard users can scroll it.
  • Sortable headers contain a <button>; the primary sort sets aria-sort on its <th>, and every sorted header's button says its direction and priority.
  • Sort changes, page changes, Load more and selection changes are announced in a polite live region.
  • Selection uses native checkboxes; the header checkbox is named "Select all 40 rows" and exposes the indeterminate state.
  • Resize handles are role="separator" with aria-orientation, aria-valuenow / min / max and a name ("Resize Customer column"); arrows resize, Enter or double-click resets.
  • Pagination is a <nav> with named Previous / Next buttons; loading sets aria-busy and hides skeletons from assistive tech.

06Limitations

  • Sorting and paging run on the client; for server-side data, control sort and pass the current page's rows.
  • Rows are not virtualised; very large data sets should page or load more.