import * as React from 'react' import { cellSelectionFeature, columnFilteringFeature, columnVisibilityFeature, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, globalFilteringFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFn_alphanumeric, sortFn_basic, tableFeatures, useTable, type Cell, type ColumnFiltersState, type ColumnDef, type ColumnVisibilityState, type PaginationState, type RowSelectionState, type SortingState, } from '@tanstack/react-table' import { useHotkeys } from '@tanstack/react-hotkeys' import { BracketsCurlyIcon, CaretDownIcon, CaretUpIcon, MagnifyingGlassIcon, RowsIcon, SlidersHorizontalIcon, StackIcon, } from '@phosphor-icons/react' import { Badge } from '~/ui/Badge' import { LandingSection, LandingSectionIntro, LandingWindow, LibraryLandingShell, } from './LibraryLanding' const tablePrompt = 'Build a TanStack Table V9 data grid for a TypeScript app. Keep it headless. Define a stable tableFeatures object with only the feature plugins, create*RowModel slots, and function registries the product needs. Use TanStack Store-backed table state, selectors, or table.Subscribe for reactive reads, and external atoms only for slices the app must own. Render semantic table elements and synchronize state to the URL or server only where the product needs it.' type TableIssue = { id: string owner: string project: string score: number status: 'active' | 'review' | 'shipped' } type StatusFilter = 'all' | TableIssue['status'] type StateMode = 'selected' | 'subscribed' | 'external' const tableWorkbenchFeatures = tableFeatures({ cellSelectionFeature, columnFilteringFeature, globalFilteringFeature, rowSortingFeature, rowPaginationFeature, rowSelectionFeature, columnVisibilityFeature, filteredRowModel: createFilteredRowModel(), sortedRowModel: createSortedRowModel(), paginatedRowModel: createPaginatedRowModel(), }) const tableRows: Array = [ { id: 'TS-732', owner: 'Tanner', project: 'Router docs', score: 98, status: 'active', }, { id: 'TS-681', owner: 'Dominik', project: 'Query cache', score: 94, status: 'review', }, { id: 'TS-644', owner: 'Kevin', project: 'Table filters', score: 91, status: 'shipped', }, { id: 'TS-612', owner: 'Ben', project: 'Virtual lists', score: 88, status: 'active', }, { id: 'TS-590', owner: 'Arthur', project: 'Column pinning', score: 84, status: 'review', }, { id: 'TS-551', owner: 'Noel', project: 'Faceted search', score: 79, status: 'shipped', }, { id: 'TS-523', owner: 'Zach', project: 'Bulk actions', score: 76, status: 'active', }, { id: 'TS-507', owner: 'Luca', project: 'Density switch', score: 72, status: 'review', }, ] const statusFilters: Array<{ label: string; value: StatusFilter }> = [ { label: 'All', value: 'all' }, { label: 'Active', value: 'active' }, { label: 'Review', value: 'review' }, { label: 'Shipped', value: 'shipped' }, ] const tableResponsibilities = ['State', 'Row processing', 'Typed APIs'] as const const developerControls = [ 'Markup & semantics', 'Styles & components', 'Events & interactions', ] as const const stateModes: Record< StateMode, { code: string; note: string; path: string } > = { selected: { code: 'const sorting = table.state.sorting', note: 'Select the state a component needs when creating its table instance.', path: 'table store → selected state → component', }, subscribed: { code: ' state.sorting}>', note: 'Move a reactive read to the smallest part of the tree that renders it.', path: 'sorting atom → subscription island → UI', }, external: { code: 'atoms: { sorting: sortingAtom }', note: 'Give an external TanStack Store atom ownership when other systems share the slice.', path: 'external atom → table feature → app', }, } const rowModelStages = [ { code: 'automatic', label: 'Core', note: 'data to rows', }, { code: 'createFilteredRowModel()', label: 'Filter', note: 'column + global', }, { code: 'createGroupedRowModel()', label: 'Group', note: 'grouped rows', }, { code: 'createSortedRowModel()', label: 'Sort', note: 'ordered rows', }, { code: 'createExpandedRowModel()', label: 'Expand', note: 'visible sub-rows', }, { code: 'createPaginatedRowModel()', label: 'Paginate', note: 'current page', }, ] as const const tableToolbox = [ { label: 'Custom features', code: 'tableFeatures({ densityFeature })', detail: 'Add state, options, and APIs through the same extension system used by built-in features.', }, { label: 'Reusable tables', code: 'createTableHook({ features, ... })', detail: 'Share typed features, options, column helpers, and registered components across a product.', }, { label: 'Devtools', code: 'useTanStackTableDevtools(table)', detail: 'Inspect table state and derived data in supported framework integrations instead of logging internals.', }, ] as const export default function TableLanding() { return ( } libraryId="table" prompt={tablePrompt} promptLabel="Copy Table prompt" >
) } function TableWorkbench() { const gridRef = React.useRef(null) const [globalFilter, setGlobalFilter] = React.useState('') const [statusFilter, setStatusFilter] = React.useState('all') const [sorting, setSorting] = React.useState([ { id: 'score', desc: true }, ]) const [columnVisibility, setColumnVisibility] = React.useState({ owner: false }) const [rowSelection, setRowSelection] = React.useState({}) const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 4, }) const columnFilters = React.useMemo( () => statusFilter === 'all' ? [] : [{ id: 'status', value: statusFilter }], [statusFilter], ) const columns = React.useMemo< Array> >( () => [ { id: 'select', enableCellSelection: false, header: ({ table }) => ( { if (input) { input.indeterminate = table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected() } }} type="checkbox" /> ), enableSorting: false, cell: ({ row }) => ( ), }, { accessorKey: 'project', header: 'Project', sortFn: sortFn_alphanumeric, }, { accessorKey: 'owner', header: 'Owner', sortFn: sortFn_alphanumeric, }, { accessorKey: 'status', header: 'Status', cell: ({ getValue }) => ( ()} /> ), filterFn: (row, columnId, value) => row.getValue(columnId) === value, sortFn: sortFn_alphanumeric, }, { accessorKey: 'score', header: 'Score', sortFn: sortFn_basic }, ], [], ) const table = useTable({ features: tableWorkbenchFeatures, columns, data: tableRows, enableCellSelection: true, enableRowSelection: true, getRowId: (row) => row.id, globalFilterFn: (row, _columnId, filterValue) => { const search = String(filterValue).trim().toLowerCase() if (!search) return true return [row.original.project, row.original.owner, row.original.status] .join(' ') .toLowerCase() .includes(search) }, onColumnVisibilityChange: setColumnVisibility, onGlobalFilterChange: setGlobalFilter, onPaginationChange: setPagination, onRowSelectionChange: setRowSelection, onSortingChange: setSorting, state: { columnFilters, columnVisibility, globalFilter, pagination, rowSelection, sorting, }, }) useHotkeys( [ { hotkey: 'ArrowUp', callback: () => table.moveCellSelection('up') }, { hotkey: 'ArrowDown', callback: () => table.moveCellSelection('down') }, { hotkey: 'ArrowLeft', callback: () => table.moveCellSelection('left') }, { hotkey: 'ArrowRight', callback: () => table.moveCellSelection('right'), }, { hotkey: 'Shift+ArrowUp', callback: () => table.extendCellSelection('up'), }, { hotkey: 'Shift+ArrowDown', callback: () => table.extendCellSelection('down'), }, { hotkey: 'Shift+ArrowLeft', callback: () => table.extendCellSelection('left'), }, { hotkey: 'Shift+ArrowRight', callback: () => table.extendCellSelection('right'), }, { hotkey: 'Mod+A', callback: () => table.selectAllCells() }, { hotkey: 'Escape', callback: () => table.resetCellSelection(true) }, ], { preventDefault: true, target: gridRef }, ) const filteredRows = table.getFilteredRowModel().rows.length const selectedRows = table.getSelectedRowModel().rows.length const selectedCells = table.getSelectedCellCount() return (
{statusFilters.map((filter) => ( ))}
{(['owner', 'status', 'score'] as const).map((columnId) => { const column = table.getColumn(columnId) return ( ) })}
{filteredRows} rows · {selectedRows} selected
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const sort = header.column.getIsSorted() return ( ) })} ))} {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) ) : ( )}
{header.isPlaceholder ? null : header.column.getCanSort() ? ( ) : ( )}
No rows match this view.
page {table.state.pagination.pageIndex + 1} /{' '} {Math.max(table.getPageCount(), 1)}
) } function OwnershipModel() { return (

TanStack Table handles

Headless table logic

{tableResponsibilities.map((responsibility) => (
{responsibility}
))}

You control

100% of the rendered result

{developerControls.map((control) => (
{control}
))}

Works with your stack

Use any component library or design system, including your own.

headless logic → your UI
) } function StateSwitchboard() { const [mode, setMode] = React.useState('subscribed') const selected = stateModes[mode] return (
{(['selected', 'subscribed', 'external'] as const).map((item) => ( ))}
{selected.code}

{selected.note}

{selected.path.split(' → ').map((step, index) => ( {index > 0 ? ( ) : null} {step} ))}
) } function RowModelPipeline() { return (
{rowModelStages.map((stage, index) => (

{String(index + 1).padStart(2, '0')}

{stage.label}

{stage.code}

{stage.note}

{index < rowModelStages.length - 1 ? (
))}
Each registered stage memoizes its derived work. Unregistered or manual stages pass the previous row model through.
) } function TableToolbox() { return (
{tableToolbox.map((tool) => (

{tool.label}

{tool.code}

{tool.detail}

))}
) } function StatusBadge({ status }: { status: TableIssue['status'] }) { return ( {status} ) } function getCellClassName(columnId: string) { if (columnId === 'select') return 'w-12 px-3 py-3' if (columnId === 'score') return 'w-16 px-3 py-3' if (columnId === 'status') return 'w-24 px-3 py-3' if (columnId === 'owner') return 'w-24 px-3 py-3' return 'min-w-0 px-3 py-3' } function getCellSelectionClassName( cell: Cell, ) { return [ 'focus-visible:outline-none', cell.getCanSelect() && 'cursor-cell select-none', cell.getIsSelected() && 'bg-[color:rgb(var(--landing-glow)/0.16)] text-text-primary', cell.getIsFocused() && 'outline outline-1 -outline-offset-2 outline-[var(--landing-accent-bright)]', ] .filter(Boolean) .join(' ') } function getCellSelectionStyle( cell: Cell, ): React.CSSProperties | undefined { if (!cell.getIsSelected()) return undefined const edges = cell.getSelectionEdges() const shadows = [ edges.top && 'inset 0 2px 0 var(--landing-accent)', edges.right && 'inset -2px 0 0 var(--landing-accent)', edges.bottom && 'inset 0 -2px 0 var(--landing-accent)', edges.left && 'inset 2px 0 0 var(--landing-accent)', ].filter(Boolean) return { boxShadow: shadows.join(', ') } }