Components

FillSheet

A form you fill in — filled key cells beside blanks, with drag-fill and per-axis resize.

A cell canvas, not a table. Every cell is addressed by row and column and holds text; a column can be any width and a row any height, dragged independently. Use it where the grid is the document — a form to fill in, a working sheet, a paste target.

A cell grows as you type, like a textarea. Enter commits and moves down; Shift+Enter puts in a line break. A column marked multiline inverts that pair — Enter breaks the line and ⌘/Ctrl+Enter commits — which is what every app with a multi-line composer does, and the column is the right place to decide it because it knows whether it holds a note or a quantity. Either way the row expands to hold the lines, and the row expands to hold however many lines there are — and shrinks again when you delete them. Newlines survive the commit; the closed cell renders them.

Click a cell and drag to select a range · type to replace it · ⌘/Ctrl+D fills the selection down · Backspace clears the selection · Tab off the last cell to add a row. Tab moves between cells rather than leaving the sheet, so Escape is the way out: once to drop the selection, again to leave entirely. In edit mode you can also point at any cell boundary and drag to resize.

That last one is a word processor's gesture, and it is why its tables are quick to fill in: your hands are already on the keyboard, and the row you want is the one after the one you just finished. It works in both modes — a row is content, not structure.

Two modes

A form is held by two different people, so mode says which one you are.

modewhocan do
edit (default)the person who makes the formdrag columns and rows to size, type, fill
fillthe person who fills it inclick, type, fill — the structure is locked

fill renders no resize grabs at all. What it withholds is the shape of the form, not its content.

One click puts you in the cell, in both modes — a word processor places the caret where you clicked, and there is no select-then-click-again step. Dragging never ends in an open editor, whether you dragged a range or a boundary: a click and a drag both finish with a mouseup, and only whether the pointer moved tells them apart.

Key cells

A column marked readOnly renders filled rather than blank, and cannot be typed into or written by a fill. That is what turns a two-column sheet into a key/value form: the filled cell is the label, the white one is yours. It is the same move the fms transaction detail makes on iOS, where a grey label cell sits beside a white value cell — the contrast, not an outline, is what makes you read it cell by cell.

Select cells

Give a column options and its cells open a Select instead of a text caret. The cell stores the option's value and shows its label — the grid stays string[][], which is what lets fill, clear and tiling keep working without knowing a cell has a type.

{ options: [{ value: "sub", label: "Захиалга" }] }

options can also be a function of the row, for a key/value form where the rows are the fields rather than the columns — the shape the fms transaction detail has:

{ options: (row) => (row === 2 ? CATEGORIES : undefined) }

A closed select cell is indistinguishable from a text cell: no chevron, no trigger, no hint that it is a select until you are in it. One click opens the list already down. format does the same job for anything else — a currency, a date — changing what is shown without changing what is stored.

Getting it into a document

⌘/Ctrl+C writes two clipboard flavours: tab-separated text, which Excel, Sheets and Numbers read as cells, and an HTML <table>, which Word, Google Docs, Pages and every rich-text editor read as a table. Given only tabs, those paste a paragraph of tabs — the HTML flavour is what makes a sheet land in a document as a document.

For everything else there are four plain functions over string[][]. No React, no DOM, so they run in a server route building an export as happily as in a click handler:

import { toCSV, toHTML, toMarkdown, toTSV } from "@crisp-ui-kit/crisp";

toHTML(value, { columns, title: "Expense claim" }); // a <table>, for .docx / Docs
toCSV(value, { columns });                          // RFC 4180, for a spreadsheet
toMarkdown(value, { columns });                     // a GFM table
toTSV(value);                                       // the clipboard's lingua franca

A column's label is its heading in an export. It is never rendered — the sheet has no header row, deliberately — so this exists purely so a document made from the sheet has one.

Each format is escaped for its own hazards: CSV quotes a field containing a comma, a quote or a newline; HTML escapes markup and turns a newline into <br>; Markdown escapes a pipe and pads short rows, both of which otherwise invent columns that were never there. TSV has no quoting at all, so a tab inside a cell is flattened to a space rather than silently shifting every row after it by a column.

Naming the sheet

label gives the grid its accessible name. There is deliberately no English default: a generic fallback would read "Sheet" aloud in the middle of a non-English form.

What it is not

FillSheet is deliberately not built on Table or DataTable, and does not share their props. Those take rows of records, own the column layout, and sort and page them. Reach for them when the data has a schema. Reach for FillSheet when the grid is the document.

There is no A/B/C lettering, no numbered row gutter and no header row, and no prop turns them on — a sheet that labels its own columns is spreadsheet software. Headings go above the sheet, where they are content.

Filling

⌘/Ctrl+C copies the selection as TSV, and pasting TSV lands as cells — so a range moves between this and Excel, Sheets or Numbers as a grid rather than as one blob of text. A paste never grows the sheet past its last row and skips readOnly columns, so a stray paste cannot quietly reshape a form somebody else has to fill in.

Select a range and press ⌘/Ctrl+D: the first row fills down over the rest of it, tiling, so a two-row pattern repeats the pair rather than stamping the last value. It writes through onValueChange like typing does, and readOnly columns are skipped rather than overwritten.

There is no drag handle. A square parked in the corner of a cell is the one mark that could not be made to sit quietly on a sheet meant to look like paper: shown always it is clutter, and revealed on hover it appears exactly when you reach for the cell.

Installation

npm install @crisp-ui-kit/crisp

Props

Prop

Type

On this page