crispmeasured from Attio

Records Table

Attio's record grid — sticky columns, typed cells, sortable headers, row selection.

A faithful rebuild of Attio's record grid, measured from the live app: 36px rows, 1px borderSubtle grid lines on both axes, a sticky checkbox column + sticky entity (first) column (with a scroll shadow), field-type icons in the header, real favicons, colour-coded tags, and typed cells. Selection, sort, and filters are self-contained.

Preview

8 records
Googlegoogle.com
182,000Mountain View, CA
Sundar Pichaitwitter.com/google29,800,000linkedin.com/company/google
307,000,000,000
Stripestripe.com
8,000San Francisco, CA
Patrick Collisontwitter.com/stripe1,200,000linkedin.com/company/stripe
16,000,000,000
Figmafigma.com
1,300San Francisco, CA
Dylan Fieldtwitter.com/figma540,000linkedin.com/company/figma
600,000,000
Notionnotion.so
800San Francisco, CA
Ivan Zhaotwitter.com/notionhq480,000linkedin.com/company/notionhq
400,000,000
Linearlinear.app
60Remote
Karri Saarinentwitter.com/linear120,000linkedin.com/company/linear
30,000,000
Vercelvercel.com
500San Francisco, CA
Guillermo Rauchtwitter.com/vercel310,000linkedin.com/company/vercel
100,000,000
OpenAIopenai.com
3,500San Francisco, CA
Sam Altmantwitter.com/openai4,200,000linkedin.com/company/openai
3,400,000,000
Anthropicanthropic.com
1,000San Francisco, CA
Dario Amodeitwitter.com/anthropicai520,000linkedin.com/company/anthropic
1,000,000,000

Everything here is live:

  • Sort — the Sort menu (or a column header) sorts by any field; the toolbar shows "Sorted by …", the header shows the direction.
  • Filter — the Filter menu is a field picker; choosing a field drops a toolbar chip that opens a condition editor (contains / is / is not / is empty / is not empty) with a value. Multiple chips AND together.
  • Column menu — click a header to open it: sort ascending/descending, filter by this field, or hide the field. Drag the header's right edge to resize (tracks the cursor).
  • Select — row checkboxes, or the header checkbox for select-all (mixed state when partial). A selection brings up the bulk-action bar — Add to list (pins ★), Send email, Run workflow, and More (Export CSV / Delete).
  • Edit — double-click any text / number / URL / date cell to edit inline; Enter commits, Esc cancels.
  • Add — the New button (or the footer row) appends a record; the header adds a column.
  • Summary footer — click any footer cell to pick its aggregation (Count all / Not empty / Sum / Average / Min / Max). Number columns default to Sum.
  • Sticky columns — scroll horizontally; the checkbox and Company columns stay pinned and cast a soft edge shadow.

Installation

bun add crisp
import "crisp/styles.css";
import { RecordsTable, type Column, type Row } from "crisp";

Usage

import { RecordsTable } from "crisp";
import { Building2, Globe, Tag } from "lucide-react";

const columns = [
  { key: "company", label: "Company", type: "entity", icon: Building2 },
  { key: "domain", label: "Domain", type: "url", icon: Globe },
  { key: "categories", label: "Categories", type: "tags", icon: Tag },
];

const rows = [
  { id: "stripe", company: { name: "Stripe" }, domain: "stripe.com", categories: ["Payments"] },
];

export function Example() {
  return <RecordsTable columns={columns} rows={rows} />;
}

Column types

Each column declares a type that decides how its cell renders:

typeCell valueRendered as
entity{ name, avatar? }Favicon/logo (letter-tile fallback) + name, sticky first column
textstringPlain text (em-dash when empty)
tagsstring[]Colour-coded pastel chips (a value keeps its colour)
numbernumberRight-aligned, toLocaleString(), tabular figures
urlstring (host)Brand-colored link to https://…
datestringMuted text

Props

PropTypeDefaultDescription
columnsColumn[]sample columnsColumn definitions (key, label, type, icon, width).
rowsRow[]sample companiesRow data keyed by column key; each needs an id.
selectedIdsstring[]Controlled selection. Omit to let the table manage it internally.
onSelectionChange(ids: string[]) => voidFires whenever the selection changes (controlled or not).
showNewbooleantrueShow the toolbar's New button (hide it when the page supplies its own).
onOpenRecord(row: Row) => voidEnables a hover button on each entity cell; fires with the row to open a Record Panel.

Notes

  • Sticky columns. The checkbox column pins at left: 0, the entity column at left: 40px; both keep their background over scrolling cells and cast a soft right-edge shadow once scrolled.
  • Controlled selection. Pass selectedIds + onSelectionChange to own the selection (e.g. to drive a side panel); otherwise it's internal. Sort, filters, edits, resize, and added rows/columns are internal state.
  • Menus (sort / filter condition / column header / summary) render into the grid via a portal so they escape the scroller's clip and the sticky-cell stacking, and animate in.
  • Still measured, not faked. Every control does real work — filters filter, Delete removes rows, New/+ append, Export downloads a CSV. Virtualization for very large datasets is the one remaining performance item.

See the whole thing composed into a page in the Companies page example.

Field-type cells

Beyond text / number / url / tags, columns can be currency, date, status, or reference — each with its own inline editor measured from Attio's cell editors. Click a Stage (status), Close date (date), or Owner (reference) cell to edit it; double-click Value (currency) to type an amount.

5 records
GGoogle — Enterprise
$120,000
FFigma — Team
$88,000
NNotion — Pro
$64,000
LLinear — Scale
$150,000
IIntercom — Starter
$30,000
const columns = [
  { key: "deal",  label: "Deal",  type: "entity" },
  { key: "stage", label: "Stage", type: "status",
    options: [
      { value: "won", label: "Won", color: "#22a565" },
      { value: "lost", label: "Lost", color: "#e5484d" },
    ] },
  { key: "value", label: "Value", type: "currency", symbol: "$" },
  { key: "close", label: "Close", type: "date" },
  { key: "owner", label: "Owner", type: "reference",
    refOptions: [{ value: "moon", label: "Moon Moon" }] },
];
  • status — a ghost Select whose options carry a colour dot; the trigger shows the selected dot + label.
  • date — a ghost DatePicker (calendar popover).
  • reference — a searchable ghost Select over refOptions, rendered back as an avatar chip.
  • currency — a right-aligned number formatted with symbol, edited inline.

On this page