Data Table
The Data Table component provides a powerful way to display and interact with tabular data, featuring row selection, expansion, sorting, and custom cell rendering. Built on TanStack Table, it offers a comprehensive solution for complex data display needs.
Column Filter Demo
Anatomy
// Detail-panel expansion<DataTable columns={columns} data={data} size="normal" getRowId={(row) => row.id} enableRowSelection enableExpanding renderSubComponent={renderSubComponent} currentSorting={sorting} onSortingChange={setSorting} pagination={paginationProps}/>
// Tree/nested rows<DataTable columns={columns} data={data} enableExpanding getSubRows={(row) => row.children} currentExpanded={expanded} onExpandedChange={setExpanded}/>Usage
import { DataTable } from "@harnessio/ui/components";import { useState } from "react";import type { ColumnDef, SortingState, RowSelectionState, ExpandedState,} from "@tanstack/react-table";
// Define your data typetype User = { name: string; email: string; status: "active" | "inactive";};
// Define your columnsconst 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> )} /> );}Grouped headers
DataTable supports multi-row column headers via TanStack Table’s nested columns API. Use a parent column with a columns array for each group. Ungrouped top-level columns (and any injected select / expander columns) automatically span both header rows.
Guidelines
- Set an explicit
idon every parent and child column when usingvisibleColumnsor column filters. - Keep sorting state keyed by leaf column
ids. Group parent headers are not interactive. - Set
enableSorting: falseon group parent columns. - Use custom
headerrenderers on leaf columns for tooltips or info icons. - Optional
meta.headerClassNameandmeta.isGroupHeaderonColumnDefcustomize header cell styling.
Row selection and expansion
When enableRowSelection or enableExpanding is enabled, the injected columns are prepended as ungrouped leaf columns and automatically span both header rows.
Props
Prop | Required | Default | Type |
|---|---|---|---|
| data | true | TData[] | |
| columns | true | ColumnDef<TData, unknown>[] | |
| size | false | 'normal' | 'normal' | 'relaxed' | 'compact' |
| variant | false | 'default' | 'default' | 'transparent' |
| getRowId | false | (row: TData) => string | |
| paginationProps | false | PaginationProps | |
| getRowClassName | false | (row: Row<TData>) => string | undefined | |
| onRowClick | false | (data: TData, index: number) => void | |
| getRowLink | false | (data: TData, index: number) => string | |
| disableHighlightOnHover | false | boolean | |
| className | false | string | |
| currentSorting | false | SortingState | |
| onSortingChange | false | OnChangeFn<SortingState> | |
| enableRowSelection | false | boolean | |
| currentRowSelection | false | RowSelectionState | |
| onRowSelectionChange | false | OnChangeFn<RowSelectionState> | |
| enableExpanding | false | boolean | |
| getRowCanExpand | false | (row: Row<TData>) => boolean | |
| getRowCanSelect | false | (row: Row<TData>) => boolean | |
| getIsRowDisabled | false | (row: Row<TData>) => boolean | |
| currentExpanded | false | ExpandedState | |
| onExpandedChange | false | OnChangeFn<ExpandedState> | |
| renderSubComponent | false | (props: { row: Row<TData> }) => React.ReactNode | |
| getSubRows | false | (originalRow: TData, index: number) => TData[] | undefined | |
| enableColumnResizing | false | boolean | |
| visibleColumns | false | string[] | |
| columnPinning | false | ColumnPinningState ({ left?: string[], right?: string[] }) |
DataTable.ColumnFilter Component
The DataTable.ColumnFilter is a sub-component that provides a dropdown menu for toggling column visibility.
Props
Prop | Required | Default | Type |
|---|---|---|---|
| columns | true | CheckboxOptions[] | |
| visibleColumns | true | string[] | |
| onCheckedChange | true | (columnName: string, checked: boolean) => void | |
| onReset | false | () => void |
useColumnFilter Hook
The useColumnFilter hook manages column visibility state with automatic localStorage persistence.
Parameters
Prop | Required | Default | Type |
|---|---|---|---|
| storageKey | true | string | |
| columns | true | CheckboxOptions[] | |
| defaultVisibleColumns | false | string[] |
Return Value
Prop | Required | Default | Type |
|---|---|---|---|
| visibleColumns | false | string[] | |
| toggleColumn | false | (columnName: string, checked: boolean) => void | |
| resetColumns | false | () => void |
Column Pinning
The DataTable component supports column pinning, allowing you to pin columns to the left or right side of the table. Pinned columns remain visible while scrolling horizontally through the table.
Usage
To enable column pinning, pass a columnPinning prop to the DataTable component with the column IDs you want to pin:
import { DataTable } from "@harnessio/ui/components";import { useState } from "react";import type { ColumnPinningState } from "@tanstack/react-table";
function TableWithPinnedColumns() { const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({ left: ["name"], // Pin 'name' column to the left right: ["status"], // Pin 'status' column to the right });
const columns = [ { id: "name", accessorKey: "name", header: "Name", size: 150, // Optional: Set column width }, { id: "email", accessorKey: "email", header: "Email", size: 200, }, { id: "status", accessorKey: "status", header: "Status", size: 100, }, ];
return ( <DataTable columns={columns} data={data} columnPinning={columnPinning} /> );}Key Features
- Left Pinning: Pin columns to the left side of the table
- Right Pinning: Pin columns to the right side of the table
- Visual Indicators: Pinned columns have subtle shadows to indicate their pinned state
- Sticky Positioning: Pinned columns remain visible during horizontal scrolling
- Multiple Columns: Pin multiple columns to either side
Best Practices
- Pin important identifier columns (like names or IDs) to the left
- Pin action columns or status indicators to the right
- Avoid pinning too many columns as it reduces the scrollable area
- Set explicit column widths using the
sizeproperty for better control
Transparent Variant
The DataTable component supports a transparent variant that removes the background and border styling, making it ideal for embedding tables in cards or within expandable table row or other containers where you want a cleaner, more integrated look.
Usage
import { DataTable, Card } from "@harnessio/ui/components";
function TransparentTable() { const columns = [ { accessorKey: "name", header: "Name" }, { accessorKey: "email", header: "Email" }, ];
const data = [ { name: "John Doe", email: "john@example.com" }, { name: "Jane Smith", email: "jane@example.com" }, ];
return ( <Card.Root> <Card.Title>Users</Card.Title> <Card.Content className="p-0"> <DataTable columns={columns} data={data} variant="transparent" size="compact" /> </Card.Content> </Card.Root> );}Key Features
- No Background: Removes the default table background for a cleaner look
- No Border: Removes outer borders while maintaining internal structure
- Expandable Rows: Perfect for embedding tables within expandable rows of another table.
- Maintains Functionality: All features (sorting, selection, expansion) work normally
Best Practices
- Use
transparentvariant when embedding tables in containers with their own backgrounds - Combine with
size="compact"for a more condensed appearance in cards - Ideal for dashboard widgets and summary tables
Adding active className to table row
The getRowClassName prop allows you to conditionally apply CSS classes to table rows based on row data. This is useful for highlighting specific rows, such as marking active items or selected states.
Usage
import { DataTable } from "@harnessio/ui/components";
function TableWithActiveRows() { const columns = [ { accessorKey: "name", header: "Name" }, { accessorKey: "email", header: "Email" }, ];
const data = [ { id: 1, name: "John Doe", email: "john@example.com", isActive: true }, { id: 2, name: "Jane Smith", email: "jane@example.com", isActive: false }, ];
return ( <DataTable columns={columns} data={data} getRowId={(row) => row.id} getRowClassName={(row) => { return row.original.isActive ? "cn-table-row-active" : undefined; }} /> );}Key Points
- The
getRowClassNamefunction receives aRow<TData>object from TanStack Table - Access row data via
row.originalto apply conditional logic - Return a string with class names or
undefinedfor no custom styling - Use design system class names (e.g.,
cn-table-row-active) for consistent styling
Disabled Rows
The getIsRowDisabled prop allows you to disable specific rows based on row data. Disabled rows have reduced opacity, show a not-allowed cursor, and cannot be clicked, selected, or expanded.
Usage
import { DataTable } from "@harnessio/ui/components";
function TableWithDisabledRows() { const columns = [ { accessorKey: "name", header: "Name" }, { accessorKey: "email", header: "Email" }, ];
const data = [ { id: 1, name: "John Doe", email: "john@example.com", status: "active" }, { id: 2, name: "Jane Smith", email: "jane@example.com", status: "inactive", }, ];
return ( <DataTable columns={columns} data={data} getRowId={(row) => row.id.toString()} getIsRowDisabled={(row) => row.original.status === "inactive"} onRowClick={(data) => console.log("Clicked:", data.name)} /> );}Key Points
- The
getIsRowDisabledfunction receives aRow<TData>object from TanStack Table - Access row data via
row.originalto apply conditional logic - Disabled rows cannot be clicked, selected, or expanded
- The “Select all” checkbox skips disabled rows
- Disabled rows are visually distinguished with reduced opacity and a not-allowed cursor
Row Links
The getRowLink prop allows you to make rows navigable by returning a URL string for each row. When provided, rows render as links. Rows disabled via getIsRowDisabled will not render as links.
Usage
import { DataTable } from "@harnessio/ui/components";
function TableWithRowLinks() { const columns = [ { accessorKey: "name", header: "Name" }, { accessorKey: "email", header: "Email" }, ];
const data = [ { id: 1, name: "John Doe", email: "john@example.com", status: "active" }, { id: 2, name: "Jane Smith", email: "jane@example.com", status: "inactive", }, ];
return ( <DataTable columns={columns} data={data} getRowId={(row) => row.id.toString()} getRowLink={(data) => `/users/${data.id}`} getIsRowDisabled={(row) => row.original.status === "inactive"} /> );}Key Points
getRowLinkreceives the row’s data and its index, and must return a string URL- Rows with a link render as navigable elements
- Disabled rows (via
getIsRowDisabled) are excluded from link rendering getRowLinkandonRowClickcan coexist; the link takes precedence for navigation
Nested Rows (Tree Mode)
The DataTable supports tree/nested row rendering via the getSubRows prop. When provided, child rows are rendered as indented rows sharing the same column structure, instead of a full-width detail panel. Rows with children show an expand/collapse chevron; leaf rows do not.
Usage
import { DataTable } from "@harnessio/ui/components";import { useState } from "react";import type { ExpandedState } from "@tanstack/react-table";
interface ProjectData { id: string; name: string; type: string; total: number; children?: ProjectData[];}
function NestedTable() { const [expanded, setExpanded] = useState<ExpandedState>({});
const data: ProjectData[] = [ { id: "platform", name: "Platform", type: "project", total: 38166, children: [ { id: "api-call", name: "API Call", type: "API_CALL", total: 20663, children: [ { id: "api-internal", name: "Internal APIs", type: "INTERNAL", total: 12400, }, { id: "api-external", name: "External APIs", type: "EXTERNAL", total: 8263, }, ], }, { id: "webhook", name: "Webhook Event", type: "WEBHOOK_EVENT", total: 11481, }, ], }, { id: "feature-flags", name: "Feature Flags", type: "project", total: 9450, }, ];
const columns = [ { accessorKey: "name", header: "Project" }, { accessorKey: "type", header: "Type" }, { accessorKey: "total", header: "Year Total (HSU)" }, ];
return ( <DataTable columns={columns} data={data} getRowId={(row) => row.id} enableExpanding getSubRows={(row) => row.children} currentExpanded={expanded} onExpandedChange={setExpanded} /> );}Key Points
getSubRowstells the table how to extract child rows from each data item- Child rows share the same column definitions as parent rows
- The expander column automatically indents based on nesting depth using the
mdspacing token - Leaf rows (no children) do not render an expand/collapse chevron
renderSubComponentis ignored whengetSubRowsis provided- Works with
currentExpandedandonExpandedChangefor controlled expansion state - Compatible with other features like sorting, row selection, and row links
Examples
Table with Custom Cell Rendering
import { DataTable, StatusBadge } from "@harnessio/ui/components";
const columns = [ { accessorKey: "name", header: "Name", }, { accessorKey: "status", header: "Status", cell: (info) => ( <StatusBadge theme={info.getValue() === "active" ? "success" : "danger"} size="sm" > {info.getValue()} </StatusBadge> ), },];Table with Conditional Row Selection
<DataTable columns={columns} data={data} enableRowSelection getRowCanSelect={(row) => row.original.status === "active"} currentRowSelection={rowSelection} onRowSelectionChange={setRowSelection}/>Table with Column Visibility Control
The DataTable component provides a built-in column filter dropdown (DataTable.ColumnFilter) and a hook (useColumnFilter) for managing column visibility with localStorage persistence.
Using the Built-in Column Filter (Recommended)
import { DataTable } from "@harnessio/ui/components";import { useColumnFilter } from "@harnessio/ui/hooks";import type { CheckboxOptions } from "@harnessio/ui/components";
// Define columns with enableHiding and unique IDsconst columns = [ { id: "name", accessorKey: "name", header: "Name", enableHiding: true, }, { id: "email", accessorKey: "email", header: "Email", enableHiding: true, }, { id: "status", accessorKey: "status", header: "Status", enableHiding: true, }, { id: "createdAt", accessorKey: "createdAt", header: "Created At", enableHiding: true, },];
// Define column options for the filter dropdownconst COLUMN_OPTIONS: CheckboxOptions[] = [ { label: "Name", value: "name" }, { label: "Email", value: "email" }, { label: "Status", value: "status" }, { label: "Created At", value: "createdAt" },];
function TableWithColumnFilter() { // Use the column filter hook with localStorage persistence const { visibleColumns, toggleColumn, resetColumns } = useColumnFilter({ storageKey: "my-table-columns", // Unique key for localStorage columns: COLUMN_OPTIONS, defaultVisibleColumns: ["name", "email", "status"], // Default visible columns });
const data = [ { name: "John Doe", email: "john@example.com", status: "active", createdAt: "2024-01-15", }, { name: "Jane Smith", email: "jane@example.com", status: "inactive", createdAt: "2024-02-20", }, ];
return ( <> {/* Column filter dropdown */} <DataTable.ColumnFilter columns={COLUMN_OPTIONS} visibleColumns={visibleColumns} onCheckedChange={toggleColumn} onReset={resetColumns} />
{/* DataTable with visible columns */} <DataTable columns={columns} data={data} visibleColumns={visibleColumns} /> </> );}Manual Column Visibility Control
You can also manually control column visibility without using the hook:
import { DataTable } from "@harnessio/ui/components";import { useState } from "react";
function TableWithManualColumnControl() { const [visibleColumns, setVisibleColumns] = useState([ "name", "email", "status", ]);
const handleColumnToggle = (columnId: string, checked: boolean) => { if (checked) { setVisibleColumns([...visibleColumns, columnId]); } else { setVisibleColumns(visibleColumns.filter((id) => id !== columnId)); } };
const handleReset = () => { setVisibleColumns(["name", "email", "status"]); };
return ( <> <DataTable.ColumnFilter columns={COLUMN_OPTIONS} visibleColumns={visibleColumns} onCheckedChange={handleColumnToggle} onReset={handleReset} />
<DataTable columns={columns} data={data} visibleColumns={visibleColumns} /> </> );}