Data Grid

A feature-rich table with column sorting, per-column filtering, row selection with an indeterminate header checkbox, custom cell rendering, and client-side pagination.

Preview

NameEmailRoleStatusJoined
Jamie Rothwelljamie@example.comAdminactive2024-01-12
Sam Torressam@example.comEditoractive2024-03-08
Alex Kimalex@example.comViewerinvited2025-01-20
Mia Chenmia@example.comEditoractive2024-07-30
Jordan Parkjordan@example.comViewersuspended2023-11-02
8 results
1 / 2

Column options

PropTypeDescription
keykeyof TMaps the column to a row property
headerstringColumn heading text
sortablebooleanEnables click-to-sort with aria-sort
filterablebooleanShows a filter input above the table
align"left" | "center" | "right"Text alignment — right for numeric columns
widthstringFixed column width (e.g., '200px')
render(value, row) => ReactNodeCustom cell renderer (badges, links, etc.)

Keyboard interactions

KeyAction
TabMove focus through filter inputs, column headers, checkboxes
Space / EnterToggle sort on focused column header
SpaceToggle row selection on focused checkbox

Implementation

DataGrid.tsx
tsx
import { DataGrid } from "@/components/ui/DataGrid";
import { Badge } from "@/components/ui/Badge";

const columns = [
  { key: "name",   header: "Name",   sortable: true, filterable: true },
  { key: "email",  header: "Email",  sortable: true, filterable: true },
  { key: "role",   header: "Role",   sortable: true },
  {
    key: "status",
    header: "Status",
    sortable: true,
    render: (value) => (
      <Badge variant={statusVariant[value]}>
        {value}
      </Badge>
    ),
  },
  { key: "joined", header: "Joined", sortable: true, align: "right" },
];

<DataGrid
  columns={columns}
  rows={users}
  pageSize={10}
  selectable
  onSelectionChange={(ids) => console.log(ids)}
/>

Guidelines

  • Use client-side sorting and filtering for datasets under 500 rows; move to server-side for larger sets.
  • Right-align numeric columns — scanning numbers is easier when they share the same decimal point column.
  • Virtualise rows (react-virtual or TanStack Virtual) when exceeding 200 rows in the DOM.
  • The header checkbox uses the indeterminate state (native ref) when only some rows are selected — never skip this.
  • Avoid more than 8 visible columns — let users customise column visibility for dense data.