Components

DataTable

A config-driven data grid — sort, filter, select, and inline-edit.

Available since@crisp-ui-kit/crisp@0.11.0

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 Selected in a BulkBar); 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 its width plus 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, a type: "number" header menu has Formatting with Grouping (Default 1,200.5 / No groups 1200.5) and Decimal places (0-4). It formats the cells and the calc footer. Pass columnFormats to control it, or defaultColumnFormats to seed it, and persist what the user picked per view with onColumnFormatChange(key, { grouping, decimals }).
  • Copy toast. Ctrl/Cmd+C shows 1 cell copied (N cells copied for a range) for five seconds when a Toast ToastProvider is mounted. Change the text with copyToastLabel(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: true on a column shows the crossed-pencil marker with a Not editable tooltip (the table's readOnlyLabel). A column can name its own marker with readOnlyLabel on 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. groupBy may return a string (the group key, rendered as before) or { key, label, icon, color }. With collapsibleGroups, collapsedGroups / defaultCollapsedGroups / onCollapsedGroupsChange fold a group, renderGroupActions adds a trailing action to its heading (usually a "+" button) and groupOrder sets 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/crisp

Props

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")}₮`,
  },
];

On this page