ComponentsData display

Table

One table for every list of records. Pass rows and columns. Turn on the features you need: sorting, search and filter chips, group rows with totals, row edit, column layout, virtual rows, and row drag.

Plain

Sortable headings, right-aligned numbers, and rows that link with getRowHref.

Customers81,284Oct 2, 2026
Invoices119,407Oct 14, 2026
Bulb plantings6312Sep 21, 2026
Suppliers51Jul 11, 2026
Wholesale orders and delivery windows140Aug 30, 2026

Grid

variant="grid" with selection (Shift+click picks a range), pinned columns, a pinned actions column, and Row edit from onRowSave.

Filters and groups

Search, filter chips, and group rows with totals. Use the Group menu to change the grouping.

Column layout

Pinned columns stay next to the checkboxes. The Columns menu hides and pins columns. Drag headers to reorder and edges to resize. persistKey keeps the layout.

Virtual rows

300 rows, with only the visible ones drawn. Drag the handles to reorder.

ReorderBedCropRows
Bed 1Tulip4
Bed 2Daffodil5
Bed 3Hyacinth6
Bed 4Crocus7
Bed 5Tulip8
Bed 6Daffodil9
Bed 7Hyacinth10
Bed 8Crocus11
Bed 9Tulip12
Bed 10Daffodil4
Bed 11Hyacinth5
Bed 12Crocus6
Bed 13Tulip7
Bed 14Daffodil8
Bed 15Hyacinth9
Bed 16Crocus10
Bed 17Tulip11
Bed 18Daffodil12
Bed 19Hyacinth4
Bed 20Crocus5
Bed 21Tulip6
Bed 22Daffodil7
Bed 23Hyacinth8

Stack on narrow screens

narrow="stack" turns rows into cards on small containers. Headers carry hints, and rows accept dropped files.

FolderOwnerFiles
Mara3
Joost18
Ines7

Sub-rows

getSubRows nests rows under a toggle.

RegionBoxes
North64
Keukenhof Growers58
Amsterdam Florals6
South42
Lisse Bulb Co.42

Caption and totals

A caption, and a footer total from column aggregates.

Orders shipped in October 2026.
CustomerBoxesTotal
Keukenhof Growers40€1,880
Lisse Bulb Co.12€420
Amsterdam Florals6€282
Hillegom Seeds22€990
Total80€3,572

URL state

useTableUrlState keeps sort, filters, search, grouping, and page in the URL, so a shared link opens the same view.

CropRegion
Keukenhof GrowersTulipNorth40€1,880Oct 2, 2026
Lisse Bulb Co.DaffodilSouth12€420Oct 4, 2026
Amsterdam FloralsTulipNorth6€282Oct 5, 2026
Total137€5,657

Server data

manual hands sorting, search, and paging to the server. Pass rowCount and read onStateChange.

Keukenhof Growers40€1,880
Lisse Bulb Co.12€420
Amsterdam Florals6€282

Loading and empty

Skeleton rows while loading, then the empty message.

Usage

Table usageTSX
import { Table, type TableColumn } from "@tulipfarm/sprout/table";

const columns: TableColumn<Order>[] = [
  { key: "customer", header: "Customer", sort: true, filter: "text" },
  { key: "crop", header: "Crop", filter: "select", group: true, options: crops },
  { key: "boxes", header: "Boxes", align: "right", sort: true, aggregate: "sum", edit: "number" },
];

<Table
  aria-label="Orders"
  data={orders}
  columns={columns}
  getRowId={(row) => row.id}
  searchable
  selectable
  onRowSave={(row, changes) => save(row.id, changes)}
/>

variant is "plain" (default) or"grid". Grid tables sit in a bordered scroll region capped at 60dvh, with a pinned header. narrow is"scroll" (default) or "stack", which turns rows into cards when the table is narrower than 36rem.

Columns

Each column reads row[key]. cell changes what shows, but sort, filter, and edit still use the raw value.sort adds a sort button. filter is"text", "number", "select","date", or "boolean". edit is"text", "number", "select","date", or "checkbox". optionsfeeds select filters and editors. group lets people group by the column. aggregate ("sum","count", "min", "max","mean") shows totals on group rows and in the footer.hint adds an info tooltip. width,pin, and hidden set the starting layout.

Rows

getRowHref or onRowClick turns the first cell into a link or button that covers the row. actions adds a pinned actions cell. onRowSave adds Edit, Save, and Cancel; if it rejects, the row stays open and shows the error.selectable adds checkboxes and callsonSelectionChange with row ids. Shift+click a checkbox to set every row since the last one picked. getSubRowsnests rows. onRowReorder adds drag handles.onRowDrop accepts files dropped on a row.pageSize adds pages, and virtual draws only the rows in view. loading shows skeleton rows, andempty shows when no rows match.

State and URLs

The table keeps its own state. Pass state to control any part, and onStateChange to hear every change. Withmanual, the server sorts, filters, and pages; passrowCount. persistKey saves column widths, order, pinning, and visibility to localStorage.

Share the view in the URLTSX
import { useTableUrlState } from "@tulipfarm/sprout/table-nuqs";

const url = useTableUrlState({ prefix: "orders-" });

<Table aria-label="Orders" data={orders} columns={columns} getRowId={(row) => row.id} {...url} />

useTableUrlState keeps sort, filters, search, grouping, and page in the URL with nuqs, so a shared link opens the same view. Installnuqs and add its adapter.

Accessibility

The table renders native table elements. Headings carryaria-sort. Checkboxes, editors, drag handles, and resize handles all work from the keyboard: drag handles use Space and the Arrow keys, and resize handles use the Arrow keys. Group rows usearia-expanded. Virtual rows set aria-rowcountand aria-rowindex. The stack layout keeps table roles and labels each value with its column name.

Theme tokens

Row dividers use --sprout-line; grid dividers and the frame use --sprout-input. Hover and grid header tints are--sprout-table-hover and --sprout-table-head-bg. Selected rows use --sprout-primary-subtle.