Overview
Blueprint’s Table2 component is a virtualized, column-resizable data grid designed for datasets with hundreds to tens of thousands of rows. It renders only visible cells, recycles DOM nodes during scroll, and supports cell renderers, selection models, and inline editing. This page covers how to set up the table correctly, define columns, render cells efficiently, and handle selection without breaking keyboard navigation.
Wrap the app in HotkeysProvider before using any Blueprint table
Blueprint’s keyboard shortcuts system requires HotkeysProvider at or near the root. Without it, the table’s built-in shortcuts (copy selection, move focus with arrow keys, expand selection with Shift) silently fail.
import { HotkeysProvider } from "@blueprintjs/core";
function App() {
return (
<HotkeysProvider>
<Router />
</HotkeysProvider>
);
}Mount HotkeysProvider once. Multiple providers produce keybinding conflicts. If another hotkey library is in use, check for conflicts before mounting Blueprint’s provider. The provider renders no DOM; it only wires the keyboard event system.
Import @blueprintjs/table/lib/css/table.css at the entry point
The table’s CSS is in a separate package. Missing it produces a table with no borders, no selection highlights, and broken column-resize handles; the JavaScript still works, which makes the bug subtle.
// main.tsx or app entry
import "normalize.css";
import "@blueprintjs/core/lib/css/blueprint.css";
import "@blueprintjs/icons/lib/css/blueprint-icons.css";
import "@blueprintjs/table/lib/css/table.css";Import order matters: normalize first, then blueprint, then table. Reversing the order lets table styles accidentally override Blueprint core styles. See blueprint for the full package list.
Define columns with Column and ColumnHeaderCell2
Each Column declares a header and a cell renderer. ColumnHeaderCell2 is the v5 header primitive; it supports a name string and a menu renderer for column actions.
import { Column, ColumnHeaderCell2, Table2 } from "@blueprintjs/table";
function DataTable({ rows }: { rows: Row[] }) {
const nameHeader = () => <ColumnHeaderCell2 name="Display Name" />;
const nameCell = (rowIndex: number) => (
<Cell>{rows[rowIndex].displayName}</Cell>
);
return (
<Table2 numRows={rows.length}>
<Column name="Name" cellRenderer={nameCell} columnHeaderCellRenderer={nameHeader} />
<Column name="Email" cellRenderer={(i) => <Cell>{rows[i].email}</Cell>} />
<Column name="Role" cellRenderer={(i) => <Cell>{rows[i].role}</Cell>} />
</Table2>
);
}Pass numRows as a number. The table uses it to size the scroll container before rendering cells. If numRows differs from the actual row count, scroll positions and virtual windowing break. Keep numRows in sync with the data array length.
Use cell renderers for custom formatting; keep them pure
A cell renderer is a function (rowIndex: number) => JSX.Element. It is called frequently: on scroll, on resize, and on re-render. Keep it pure and cheap.
const statusCell = (rowIndex: number) => {
const status = rows[rowIndex].status;
return (
<Cell intent={status === "error" ? Intent.DANGER : Intent.NONE}>
<Tag minimal intent={status === "error" ? Intent.DANGER : Intent.SUCCESS}>
{status}
</Tag>
</Cell>
);
};Avoid fetching data inside a cell renderer. Fetch before the table renders; pass data as props or close over a stable array. Unstable references inside a renderer (inline objects, arrow functions created on every render) cause the table to re-render more cells than necessary. Use useCallback for renderers that depend on changing state. See react-performance for the memoization rules.
Configure selection with selectionModes and an onSelection callback
Table2 supports four selection modes: none, rows, columns, and cells. Pass the appropriate constant from SelectionModes.
import { SelectionModes } from "@blueprintjs/table";
<Table2
numRows={rows.length}
selectionModes={SelectionModes.ROWS_AND_CELLS}
onSelection={(selections) => {
const selected = selections.map((r) => rows[r.rows?.[0] ?? 0]);
setSelectedRows(selected);
}}
>SelectionModes.ROWS_ONLY is common for list-style grids where row-level actions (delete, export) should act on a set of rows. SelectionModes.NONE disables interactive selection for read-only tables. Keyboard selection (Shift+Click, Ctrl+A) works automatically once the mode is set; no additional event handling is needed.
Handle large datasets with row loading and sparse data
For datasets fetched in pages, render a loading cell while data is in flight.
const cell = (rowIndex: number) => {
const row = rows[rowIndex];
if (!row) return <Cell loading />;
return <Cell>{row.name}</Cell>;
};The loading prop renders a skeleton shimmer. Load rows into the array as they arrive; the table re-renders only the cells that changed. For very large datasets (100k+ rows), keep numRows as the total count and fill the array lazily from a windowed fetch. This avoids a DOM with 100k cells on the initial render.