DataTable
A config-driven data grid — sort, filter, select, and inline-edit.
The batteries-included, API-driven data grid: pass columns + data and get sorting, a search filter, row selection, and inline editing — all controllable via props. Built on the composable Table primitives, so it inherits the same look.
Try it — sort by any header, search, select rows, and click a Domain cell to edit it.
Use this for real application tables — typed-cell pickers, sticky columns, selection, inline edit, a bulk bar and a summary footer are all built in; use Table when you want to lay out cells by hand.
Attio grid behaviours
The grid above is bounded (height), so its header and calc footer pin; scroll it sideways and the primary column's shadow fades in while the footer's first cell stays put.
- Row selection. Click a row checkbox, then Shift+Click another to select the whole range (
6 Selectedin aBulkBar); Ctrl/Cmd+Click toggles one row; Shift+Arrow from a selected cell extends the selection by row. - Checkbox placement (0.69.0). With
selectable, the row checkbox sits inside the first column, as in Attio (crisp#1313): the select-all box is in the first header cell beside the title and each row's box is in its first data cell, before the value. There is no separate leading checkbox column. The first column's rendered width is itswidthplus a 26px gutter. Code that targeted.crisp-datatable-col-check, or counted the leading columns of a selectable table, should target the first column and expect one column fewer. - Number formatting. With
columnMenu, atype: "number"header menu hasFormattingwith Grouping (Default 1,200.5/No groups 1200.5) and Decimal places (0-4). It formats the cells and the calc footer. PasscolumnFormatsto control it, ordefaultColumnFormatsto seed it, and persist what the user picked per view withonColumnFormatChange(key, { grouping, decimals }). - Copy toast. Ctrl/Cmd+C shows
1 cell copied(N cells copiedfor a range) for five seconds when a ToastToastProvideris mounted. Change the text withcopyToastLabel(count); with no provider nothing shows. - Cell expansion and editing. Selecting a truncated text cell (click or arrow keys) grows it over the rows below, tinted, clamped at six lines. Enter or double-click opens a textarea that grows with its text (Enter commits, Shift+Enter adds a line, Escape cancels).
- Read-only columns.
readOnly: trueon a column shows the crossed-pencil marker with aNot editabletooltip (the table'sreadOnlyLabel). A column can name its own marker withreadOnlyLabelon the column (crisp#1216), so a derived column can say why it cannot be typed into ("Calculated, not editable") while a stored field keeps the generic text. The label is both the tooltip and the marker's accessible name. - Row props.
getRowProps(row)returns props spread onto that row's<tr>(crisp#1234):role,data-*,tabIndex, and key, focus and pointer handlers. The table's own props (data-selected,onClick, drag and hover handlers,style) win on a clash, so use handlers it does not set (onKeyDown,onFocus,onPointerEnter,onClickCapture). - Collapsible groups.
groupBymay return a string (the group key, rendered as before) or{ key, label, icon, color }. WithcollapsibleGroups,collapsedGroups/defaultCollapsedGroups/onCollapsedGroupsChangefold a group,renderGroupActionsadds a trailing action to its heading (usually a "+" button) andgroupOrdersets the order of the groups. - Advanced filters live on TableToolbar; the unsaved-view bar on SavedViews.
Unpinned first column
With stickyFirst={false} the first column is not pinned, but it is still the row header. Stripe is selected above: its Company cell carries the same selected tint as the cells beside it, and hovering a selected row washes every cell of it.
Virtualization
10,000 rows × 40 columns — only the rows and columns inside the viewport (plus
a small overscan) are ever in the DOM. Set height (a pixel number or any
CSS length) to opt a table into a bounded, internally-scrolling viewport;
that's the same thing that turns on ROW virtualization. COLUMN
virtualization needs no prop — it's always on, since the grid already has a
determinate horizontal scroll container. A table with fewer rows than fit in
one viewport renders exactly as it did before either existed. Sticky columns
(stickyFirst, stickyLast), the sticky header and calc footer, column
resize, keyboard navigation (arrowing to an off-screen cell scrolls it into
view), selection, inline editors, groupBy, and row drag-reorder all keep
working through it — see #577.
Installation
npm install @crisp-ui-kit/crispProps
Prop
Type
Column resize
Dragging a header's right edge resizes the column and stops at 100px, Attio's measured floor (a drag from 180px runs 255 → 145 → 100 and never below; it was 64px before carrot-systems/crisp#1158). Pass minColumnWidth to change the floor. Only the drag is clamped: a column given a narrower width still renders at that width until someone drags it. Persist what the user chose with onColumnResize and seed it back with defaultColumnWidths.
DataTableColumn<T>
key, header, and optional sortable, filterable, editable, align, width, render(value, row), accessor(row) (derive the sort/filter/display value), and format(value, row) (derive the display string).
A header wider than its column truncates with an ellipsis — give a column a width that's tight for its label (or let a consumer resize one narrow via the columnMenu) and it clips instead of spilling into the next column. Once it actually overflows, hovering (or focusing) it opens a Tooltip with the full text — no title attribute, matching Attio's own header (measured live). Give a referencedType ({ icon, label }) to a column whose values reference another record type (e.g. a "Primary location" column of Country records) and that tooltip appends the referenced type's icon and name after the label, switching to Attio's second, reference-column tooltip chrome.
Precedence: render (a full custom node) beats format (a display string, still passed through the built-in type renderer — so a currency/number column keeps its right-alignment and cell chrome) beats the built-in type renderer.
format vs accessor: accessor derives the value used for sorting and filtering; format only changes what's displayed and never affects sort order — a currency column formatted with grouping ("9", "80", "700") still sorts numerically (9 < 80 < 700), not lexicographically on the formatted string.
const columns: DataTableColumn<Row>[] = [
{
key: "amountMnt",
header: "Amount",
type: "currency",
sortable: true,
// display only — sort stays on the raw number
format: (v) => `${(v as number).toLocaleString("mn-MN")}₮`,
},
];