| Name | Reference | Stock |
|---|---|---|
<Spreadsheet.Root>
<Spreadsheet.Columns>
<Spreadsheet.Column />
<Spreadsheet.Column width={130} />
</Spreadsheet.Columns>
<Spreadsheet.Header>
<tr>
<Spreadsheet.Head>Name</Spreadsheet.Head>
<Spreadsheet.Head>Reference</Spreadsheet.Head>
</tr>
</Spreadsheet.Header>
<Spreadsheet.Body>
{rows.map((row) => (
<Spreadsheet.Row key={row.id}>
<Spreadsheet.Cell>
<Input value={row.name} onChange={…} />
</Spreadsheet.Cell>
<Spreadsheet.Cell>
<Input value={row.reference} onChange={…} />
</Spreadsheet.Cell>
</Spreadsheet.Row>
))}
</Spreadsheet.Body>
</Spreadsheet.Root>;The grid never sorts or reorders your rows itself — it reports the intent and you reorder the array, so the data stays the single source of truth.
API
Spreadsheet.Root
| Prop | Type | Default | |
|---|---|---|---|
columnOrder | string[] | — | Controlled column order, as the list of columnIds currently rendered.
Providing it (with onColumnOrderChange) makes identified header cells
draggable and Alt+arrow movable; the consumer re-renders columns, header
and body cells alike, in the received order. |
columnSizing | Record<string, number> | — | Column widths by columnId, written by resizable header cells. Leave
undefined for internal state; pass it (with onColumnSizingChange) to
control or persist the widths. Any entry switches the table to
table-layout: fixed. |
containerClassName | string | — | Classes for the scroll container around the table, which is where a bounded
height belongs — className goes to the <table> itself and cannot scroll. |
gridNavigation | boolean | — | Opt-in spreadsheet keyboard layer, upgrading the table to role="grid":
the body cells become a single roving tab stop (arrows/Home/End/PageUp/
PageDown move, Enter/F2/typing edits, Escape comes back), Shift+arrows or
pointer drag select a rectangle and Cmd/Ctrl+C copies it as TSV (the
value prop on SpreadsheetCell overrides what's copied). Off by
default: the phase-1 native Tab order stays the baseline. |
mobileAddRow | ReactNode | — | Rendered full-width under the mobile card stack - put an
SpreadsheetMobileAddRow here, wired like your SpreadsheetAddRow. |
onColumnOrderChange | ((order: string[]) => void) | — | Called with the new columnId order once a drag or Alt+arrow move lands. |
onColumnSizingChange | ((sizing: Record<string, number>) => void) | — | Called on every drag frame of a resize; debounce before persisting. |
onPasteData | ((data: SpreadsheetPasteData) => void) | — | Grid-mode paste (Cmd/Ctrl+V on a cell in navigation mode): receives the parsed TSV matrix and the target's top-left position. The table never mutates anything - apply the values to your row array, extending it when the paste overflows (multi-row pastes from Excel/Sheets are the target use case). |
onRowMove | ((fromIndex: number, toIndex: number) => void) | — | Enables SpreadsheetDragHandle cells: called with the dragged row's
index and its target position. Apply the move to your row array. |
onSortChange | ((sort: SpreadsheetSort | null) => void) | — | Called with the next sort, or null at the end of the tri-state cycle. |
renderMobileRow | ((index: number) => ReactNode) | — | Card content for one row below the md breakpoint. When provided (with
rowCount), the table is replaced on mobile by a vertical stack of
cards - a dense grid is not editable with a thumb. Render the SAME
controlled fields in a labelled vertical layout (Field + regular
controls, not flattened) plus the row's delete button; mobileAddRow
takes over from SpreadsheetAddRow under the stack. |
rowCount | number | — | Total number of rows - only read by the mobile card mode. |
sort | SpreadsheetSort | null | — | Controlled sort state for sortable header cells. The table never sorts
the rows itself: reorder your row array when this changes (a sorted view
over indexed form rows would break their binding). |
stickyFirstColumn | boolean | — | Pins each row's first cell while the table pans horizontally, keeping the
row label readable on narrow screens; an edge shadow appears once the
container is actually scrolled. Render a read-only part
(SpreadsheetCellText) as the first column - a panning grid under a
pinned editable cell is disorienting. |
stickyHeader | boolean | — | Pins the header row while the body scrolls. Only useful together with a
max-h-* on the container (via containerClassName). |
Plus the DOM props of the element it renders. Source: spreadsheet/components/spreadsheet-root.tsx.
Sorting, resizing and reordering are all reported, never applied: the table hands you the intent and you reorder the array. A sorted view over indexed form rows would break their binding, which is why this is a rule rather than a default.
Spreadsheet.Columns
Plus every <colgroup> prop. Source: spreadsheet/components/spreadsheet-columns.tsx.
Spreadsheet.Column
| Prop | Type | Default | |
|---|---|---|---|
columnId | string | — | Matches the columnId of the header cell that resizes this column. |
width | string | number | — | Starting width. A resize writes over it, matched by columnId. |
Plus every <col> prop. Source: spreadsheet/components/spreadsheet-column.tsx.
Widths live on <col> elements rather than header classes, so resizing and
table-layout: fixed have one place to write to.
Spreadsheet.Header
Plus every <thead> prop. Source: spreadsheet/components/spreadsheet-header.tsx.
Spreadsheet.Head
| Prop | Type | Default | |
|---|---|---|---|
columnId | string | — | Stable column identifier for sorting/resizing/reordering. |
resizable | boolean | false | Adds a resize handle on the right edge. Needs columnId. |
sortable | boolean | false | Turns the header into a tri-state sort button. Needs columnId. |
Plus every <th> prop. Source: spreadsheet/components/spreadsheet-head.tsx.
Spreadsheet.Body
Plus every <tbody> prop. Source: spreadsheet/components/spreadsheet-body.tsx.
Spreadsheet.Row
| Prop | Type | Default | |
|---|---|---|---|
invalid | boolean | — | Draws the invalid ring on the row's cells. The row is the right place for it: the controls inside sit on transparent backgrounds. |
Plus every <tr> prop. Source: spreadsheet/components/spreadsheet-row.tsx.
Spreadsheet.Cell
| Prop | Type | Default | |
|---|---|---|---|
value | string | — | What gridNavigation copy serializes for this cell instead of the inner
control's DOM value - required for controls whose DOM value isn't the
data (Checkbox, Switch), useful whenever the copied text should differ
from what the control displays. |
Plus every <td> prop. Source: spreadsheet/components/spreadsheet-cell.tsx.
Spreadsheet.CellText
Plus every <td> prop. Source: spreadsheet/components/spreadsheet-cell-text.tsx.
A read-only cell. Use it for the first column when stickyFirstColumn is on —
a grid panning under a pinned editable cell is disorienting.
Spreadsheet.CellImage
| Prop | Type | Default | |
|---|---|---|---|
onFileSelect | (file: File) => void | required | Hands over the picked File. Upload it, then feed the URL back as src. |
pickLabel | string | required | Accessible name of the picker button, e.g. "Import an image, row 3". |
accept | string | "image/*" | accept for the hidden file input, e.g. "image/png,image/jpeg". |
alt | string | "" | Alt text of the thumbnail - the row's label, not "image". |
disabled | boolean | false | Blocks all three import paths: the picker, the drop and the paste. |
onRemove | (() => void) | — | Omit to make the image non-clearable (no remove affordance is drawn). |
removeLabel | string | — | Accessible name of the remove button. Only drawn when onRemove is passed. |
src | string | null | — | Displayed thumbnail URL; empty or null renders the placeholder. |
uploading | boolean | false | Swaps the thumbnail for a spinner while your upload runs. |
value | string | "" | What gridNavigation copy serializes for this cell. |
Plus every <td> prop. Source: spreadsheet/components/spreadsheet-cell-image.tsx.
Imports a file three ways: click, drop onto the cell, or paste while the cell is
focused. It neither uploads nor holds the file — onFileSelect hands you the
File and you feed the resulting URL back as src.
Spreadsheet.DragHandle
| Prop | Type | Default | |
|---|---|---|---|
index | number | required | The row's current position. Needs onRowMove on the table to do anything. |
Plus every <td> prop. Source: spreadsheet/components/spreadsheet-drag-handle.tsx.
Render it as the row's first cell. Keyboard: Space grabs, arrows pick the position, Space drops, Escape cancels.
Spreadsheet.RowActions
Plus every <td> prop. Source: spreadsheet/components/spreadsheet-row-actions.tsx.
Spreadsheet.AddRow
| Prop | Type | Default | |
|---|---|---|---|
colSpan | number | required | Must count ALL columns, the drag-handle and actions columns included. |
Plus every <button> prop. Source: spreadsheet/components/spreadsheet-add-row.tsx.
Spreadsheet.MobileAddRow
Plus every <button> prop. Source: spreadsheet/components/spreadsheet-mobile-add-row.tsx.
Spreadsheet.VirtualRows
| Prop | Type | Default | |
|---|---|---|---|
children | (index: number) => ReactNode | required | Renders ONE row, by absolute index. Keep your usual Spreadsheet.Row markup here. |
count | number | required | Total number of rows, mounted or not. |
estimatedRowHeight | number | 33 | Pixel height of one row, borders included. |
overscan | number | 8 | Extra rows rendered on both sides of the visible window. |
Plus the DOM props of the element it renders. Source: spreadsheet/components/spreadsheet-virtual-rows.tsx.
Worth it from about a hundred rows. It rules out row dragging — positional drop
targets do not survive windowing — and needs fixed-height rows plus a bounded
containerClassName.
Spreadsheet.Skeleton
| Prop | Type | Default | |
|---|---|---|---|
columns | number | required | Must match the real column count, or the placeholder rows will not line up. |
rows | number | 3 | How many placeholder rows to draw. |
Plus the DOM props of the element it renders. Source: spreadsheet/components/spreadsheet-skeleton.tsx.
Sized to the real rows (32px + 1px) so the swap to live data shifts nothing.