Quick start

An editable, virtualized grid: keyboard navigation, paste, drag to reorder.

An editable, virtualized grid your users will mistake for a native app.

Ops screens always end up wanting one: arrow keys that move like the spreadsheet your users came from, paste that accepts a whole block of cells, rows that drag into a new order, and virtualisation so ten thousand rows scroll like ten. Nobody should rent that as a SaaS widget. This one is source-shipped: install it, read the keyboard grid, and when your product needs a cell type nobody planned for, open the file and add it.

NameReferenceStock

Install

bun add @voila.dev/ui @tanstack/react-virtual
globals.css
@import "@voila.dev/ui/styles/sources.css";

Or copy the source — it's the same files. Peers: react@19, react-dom@19, tailwindcss@4.

The 3-minute win

A three-column stock grid. Your inputs go inside the cells, your state stays yours.

import { Spreadsheet } from "@voila.dev/ui/spreadsheet";
 
<Spreadsheet.Root>
	<Spreadsheet.Columns>
		<Spreadsheet.Column />
		<Spreadsheet.Column width={130} />
		<Spreadsheet.Column width={100} />
	</Spreadsheet.Columns>
	<Spreadsheet.Header>
		<tr>
			<Spreadsheet.Head>Name</Spreadsheet.Head>
			<Spreadsheet.Head>Reference</Spreadsheet.Head>
			<Spreadsheet.Head>Stock</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.Cell>
					<Input type="number" value={row.stock} onChange={} />
				</Spreadsheet.Cell>
			</Spreadsheet.Row>
		))}
	</Spreadsheet.Body>
</Spreadsheet.Root>;

Mental model

Spreadsheet.Root owns the state and the keyboard grid; every other part draws into it. Cells hold your own inputs, so validation and formatting live in your code, not in a cell renderer API. The grid never sorts or mutates your rows: it reports intent (a paste, a reorder) and you update the array, so the data stays the single source of truth. For rows that are read and exported rather than edited, the answer is the other package; see Which one do I need?

Page map