Skip to content

Data Table

The DataTable component provides a data-driven way to display and interact with tabular data, featuring row selection, expansion, sorting, and custom cell rendering. Built on TanStack Table, you describe your columns and hand it your data — it builds the underlying Table for you. Reach for the composable Table instead when you want to lay out rows and cells by hand.

Variations

Column visibility

Pair the useColumnFilter hook with DataTable.ColumnFilter to let people toggle which columns show. Columns must set enableHiding: true and a unique id; the hook persists the choice to localStorage.

Grouped headers

Build multi-row column headers with TanStack Table’s nested columns API — a parent column with a columns array becomes a header group. Give every parent and child an explicit id, set enableSorting: false on parent columns, and keep sorting state keyed by leaf column ids. Ungrouped top-level columns (and injected select/expander columns) span both header rows automatically.

Nested rows (tree mode)

Pass getSubRows to render child rows as indented rows sharing the same columns, instead of a full-width detail panel. Rows with children show an expand/collapse chevron; leaf rows don’t. renderSubComponent is ignored when getSubRows is provided.

Column resizing

Enable live column resizing with enableColumnResizing. Only leaf columns expose a handle on the header boundary. Group parent widths follow the combined width of their visible children. Opt a leaf out with enableResizing: false, and configure sizing with size, minSize, and maxSize.

Resize by dragging the handle with a mouse or touch, or by focusing the handle and using the arrow keys (Shift for larger steps). Sorting stays on the header label — using the handle does not toggle sort.

enableResizing: false prevents direct resizing and leaves that column’s sizing state unchanged when a neighboring column is resized. It does not lock the rendered pixel width in a full-width table: the browser’s automatic table layout may redistribute available space. size is the preferred width, minSize is the lower bound, and maxSize limits direct resizing; set minSize equal to size when the column must not shrink below its configured width.

Guidelines:

  • Turn resizing on at the table with enableColumnResizing. It is off by default.
  • Use ColumnDef.enableResizing: false to remove a leaf column’s resize handle and prevent direct resizing. This does not lock its rendered width in an auto-layout table.
  • Set size, minSize, and maxSize on leaf columns. Use minSize: size when a non-resizable column must not shrink below its configured width. Group headers do not get handles; their width is derived from child leaves.
  • Injected select and expander columns are not resizable.

Return a URL from getRowLink to make rows navigable — rows then render as links. Rows disabled via getIsRowDisabled are excluded from link rendering. getRowLink and onRowClick can coexist; the link takes precedence for navigation.

Active row styling

Use getRowClassName to conditionally apply classes to rows based on row data — useful for marking active or highlighted rows. The function receives a TanStack Row; read row.original and return a class string (e.g. cn-table-row-active) or undefined.

Disabled rows

Return true from getIsRowDisabled to disable specific rows. Disabled rows have reduced opacity, show a not-allowed cursor, and cannot be clicked, selected, or expanded. The “Select all” checkbox skips them.

Transparent variant

Set variant="transparent" to remove the background and border styling — ideal for embedding a table in a card or inside an expandable row. All features keep working; pair with size="compact" for a condensed look.

Column pinning

Pass a columnPinning state ({ left, right }) to pin columns to either side so they stay visible during horizontal scrolling. Pinned columns get a subtle shadow. Set explicit column size values for the best control, and avoid pinning so many columns that the scrollable area shrinks too far.

Sticky headers

Set stickyHeader (paired with maxHeight) to render the table in a single scroll viewport so header cells stick to the top while the body scrolls — staying fully opaque, even on sortable-header hover. Sticky offsets support up to two stacked header rows and compose with column pinning.

Row pinning

Opt in to bottom row pinning via TanStack’s rowPinning state — designed for a grand-total row that stays visible at the bottom. Append the total row to data and list its getRowId id in rowPinning.bottom; pair with stickyHeader so the header and total frame the scrolling body. Bottom pinning supports a single row — if multiple ids are listed, only the last one renders.

Installation

import { DataTable } from "@harnessio/ui/components";
import { useState } from "react";
import type {
ColumnDef,
SortingState,
RowSelectionState,
ExpandedState,
} from "@tanstack/react-table";
// Define your data type
type User = {
name: string;
email: string;
status: "active" | "inactive";
};
// Define your columns
const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: "Name",
enableSorting: true,
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "status",
header: "Status",
},
];
function MyTable() {
// State for sorting, selection, and expansion
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [expanded, setExpanded] = useState<ExpandedState>({});
// Your data
const data = [
{ name: "John Doe", email: "john@example.com", status: "active" },
{ name: "Jane Smith", email: "jane@example.com", status: "inactive" },
];
return (
<DataTable
columns={columns}
data={data}
getRowId={(row) => row.email}
currentSorting={sorting}
onSortingChange={setSorting}
enableRowSelection
currentRowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
enableExpanding
currentExpanded={expanded}
onExpandedChange={setExpanded}
renderSubComponent={({ row }) => (
<div className="p-cn-md">
<p>Additional details for {row.original.name}</p>
</div>
)}
/>
);
}