Skip to content

React Tables prompt

Rules that make your coding agent build data tables on TanStack Table v9 and shadcn/ui's Table parts: typed columns, server pagination in the URL, selection and real table markup.

What it makes your agent good at

Agents still write TanStack Table v8: useReactTable, getCoreRowModel and sortingFn, which v9 no longer has. They rebuild columns on every render, sort one server page on the client and select rows by index. This prompt gives them v9 as installed: registered features and row models, controlled state that lives in the URL, ids that survive paging, and a real table screen readers can sort.

  1. v9, not v8useTable with a module-scope tableFeatures object: each feature, row model and named filter or sort function registered, no getCoreRowModel.
  2. Typed, stable columnscreateColumnHelper and helper.columns keep each value type; columns, features and data keep their identity between renders.
  3. Server data in the URLManual pagination, sorting and filtering with rowCount, state parsed from search params, and the page index reset when filters change.
  4. Selection that survives paginggetRowId with real ids, page-scoped header checkboxes with v9's indeterminate rule, and bulk actions on getSelectedRowIds.
  5. Faceted filters and searchGlobal search, faceted counts from getFacetedUniqueValues and multi-select filters with arrHas, each registered as a feature.
  6. Markup screen readers can useReal table elements, sort buttons with aria-sort, labelled checkboxes, and loading and empty states that keep the layout.

The React Tables rules

115 lines, current to TanStack Table 9.3 and React 19.3. Last reviewed October 23, 2026. Plain Markdown, no front matter: each agent's is below.

Download

react-tables.md

# React Tables

You are working in a React 19 project with TypeScript, TanStack Table v9 (`@tanstack/react-table`) and shadcn/ui-style Table parts (`Table`, `TableHeader`, `TableRow`, `TableHead`, `TableCell`…). Write code for the versions in `package.json`: v9 changed how a table is built, how state is read and several names, and most examples you remember are v8.

## Before you write code

- Check the major version of `@tanstack/react-table`. These rules are for v9; on a v8 project follow v8's docs and never mix the two APIs.
- v9 ships its own guides: read `node_modules/@tanstack/react-table/skills/` (getting-started, table-state, migrate-v8-to-v9) and the feature you are adding in `node_modules/@tanstack/table-core/skills/table-features/references/`. Exact signatures: `node_modules/@tanstack/table-core/dist/features/<feature>/*.types.d.ts`.
- Reuse the project's Table parts (`components/ui/table.tsx` or similar) and the shape of its existing data tables.

## Build the table

A v9 table has only the core row model until you register features. Register each feature, then its row-model slot and the named functions its columns use, in one module-scope `tableFeatures` call:

```tsx
"use client";

import {
  columnFilteringFeature, createColumnHelper, createFilteredRowModel,
  createPaginatedRowModel, createSortedRowModel, filterFn_includesString,
  rowPaginationFeature, rowSortingFeature, sortFn_alphanumeric,
  tableFeatures, useTable,
} from "@tanstack/react-table";

type Payment = { id: string; email: string; amount: number };

const features = tableFeatures({
  columnFilteringFeature,
  rowSortingFeature,
  rowPaginationFeature,
  filteredRowModel: createFilteredRowModel(),
  sortedRowModel: createSortedRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  filterFns: { includesString: filterFn_includesString },
  sortFns: { alphanumeric: sortFn_alphanumeric },
});

const helper = createColumnHelper<typeof features, Payment>();
const columns = helper.columns([
  helper.accessor("email", { header: "Email", filterFn: "includesString", sortFn: "alphanumeric" }),
  helper.accessor("amount", { header: "Amount" }),
]);

export function PaymentsTable({ data }: { data: Payment[] }) {
  const table = useTable({ features, columns, data, getRowId: (row) => row.id });
  // render table.getHeaderGroups() and table.getRowModel().rows
}
```

- Keep `features` and `columns` at module scope (`useMemo` them if they depend on props). Keep `data` stable: state, a query result or a prop, never `rows.filter(…)` or `data ?? []` inline; hoist `const EMPTY: Payment[] = []`. A new array each render reruns every row model.
- Type columns through `createColumnHelper<typeof features, TData>()` and wrap the list in `helper.columns([...])` so each column keeps its value type. Never annotate the array as `ColumnDef<…>[]`.
- `helper.accessor("key")` for data (deep keys work: `"customer.name"`), `helper.accessor((row) => …, { id: "domain" })` for derived values, `helper.display({ id: "select" })` for checkbox and action columns, `helper.group` for grouped headers.
- Accessors return primitives (string, number, Date); sorting and filtering run on them. Formatting and JSX go in `cell`, with formatters (`Intl.NumberFormat`) hoisted out of render.
- Call row, cell, column and header methods on their object: `row.getValue("email")`, `rows.map((row) => row.getVisibleCells())`. v9 put them on prototypes, so `const { getValue } = row` or a bare `row.getVisibleCells` loses `this`. Destructuring the render context (`cell: ({ getValue, row }) => …`) is fine.
- Render with `<table.FlexRender header={header} />` and `<table.FlexRender cell={cell} />`, skipping `header.isPlaceholder`.
- Type column meta with a `columnMeta: metaHelper<{ align?: "start" | "end" }>()` slot, not global `declare module` merging.
- When several tables share features and cell components, define them once with `createTableHook` at module scope (`useAppTable`, `createAppColumnHelper`); one-off tables use `useTable`.

## State

- Let the table own state by default; seed it with `initialState` (`{ pagination: { pageIndex: 0, pageSize: 20 } }`).
- To control a slice, pass both the value and its callback: `state: { sorting }` with `onSortingChange: setSorting`. A React setter accepts the updater as is; anywhere else apply `functionalUpdate(updater, previous)`.
- Read state in render from `table.state.sorting`. `table.atoms.<slice>.get()` and `table.store.state` are snapshots that never re-render.
- `useTable` re-renders on every state change. For large tables pass a selector as its second argument, `(state) => ({ pagination: state.pagination })`, and subscribe lower with `<table.Subscribe selector={…}>`. Under the React Compiler, a child given only `row` that reads `row.getIsSelected()` won't update: subscribe inside it or pass the value as a prop.

## Sorting, filtering and pagination

- Sorting: a `<button>` inside the header cell when `header.column.getCanSort()`, wired to `getToggleSortingHandler()`, showing `getIsSorted()` (`"asc" | "desc" | false`). Custom comparators return ascending order; the table reverses them. Use `sortUndefined: "last"` and `enableSortingRemoval: false` when the product needs them.
- Column filters: `column.setFilterValue(value)` and `column.getFilterValue()`; an empty value removes the filter.
- Global search: add `globalFilteringFeature` after `columnFilteringFeature`, set `globalFilterFn: "includesString"`, call `table.setGlobalFilter(value)`, and exclude columns with `getColumnCanGlobalFilter`.
- Faceted filters (status with counts): add `columnFacetingFeature` with `facetedRowModel: createFacetedRowModel()` and `facetedUniqueValues: createFacetedUniqueValues()` (`facetedMinMaxValues` for numeric ranges). `column.getFacetedUniqueValues()` is a `Map` of value to count. A multi-select over a single-value column uses `filterFn: "arrHas"` (register `filterFn_arrHas`) with an array filter value; numeric ranges use `inNumberRange`.
- Client pagination: `rowPaginationFeature` with `paginatedRowModel`; controls use `getCanPreviousPage()`, `getCanNextPage()`, `setPageIndex`, `setPageSize` and `getPageCount()`.

## Server-side data and the URL

- When the dataset is too large to send whole or the server owns the query, set `manualPagination`, `manualSorting` and `manualFiltering` together, pass `rowCount` (the server total), and pass exactly the processed page as `data`. Manual flags never fetch anything; they only skip client processing. Never sort or filter one loaded page on the client while the server paginates.
- Make the URL the single owner of page, page size, sort and filters: parse the search params with a schema (clamp the page, allow only sortable column ids), pass them as controlled `state`, and in each `on…Change` apply `functionalUpdate(updater, current)` and write the result back with a replace navigation. Reset `pageIndex` to 0 when sorting or filters change: manual pagination does not do it for you. Debounce text filters that trigger requests.
- With server rendering, query on the server from the search params and pass `rows`, `rowCount` and the parsed state to the client table; navigate inside `startTransition` for a pending state. With a client data library, put every server-owned slice in the query key and keep the previous page shown while the next loads.
- Never copy fetched rows into state with an effect: pass the query result straight to `data` with a stable empty fallback. Facet counts computed on the client only cover the loaded page; fetch them from the server in manual mode.

## Visibility, selection and bulk actions

- Column visibility needs `columnVisibilityFeature`. Render `row.getVisibleCells()` (`getAllCells()` includes hidden ones) and give full-width rows `colSpan={table.getVisibleLeafColumns().length}`. Build the menu from `table.getAllLeafColumns().filter((column) => column.getCanHide())`. Only an explicit `false` hides a column; `enableHiding: false` only forbids hiding it.
- Row selection needs `rowSelectionFeature` and `getRowId: (row) => row.id`: index ids break across pages, sorting and refetches.
- Header checkbox: checked from `table.getIsAllPageRowsSelected()`, indeterminate when `table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()` (v9's "some" includes "all"), toggled with `table.toggleAllPageRowsSelected(checked)`. Base UI's Checkbox takes `indeterminate`, Radix's `checked="indeterminate"`.
- Row checkbox: `row.getIsSelected()`, disabled when `!row.getCanSelect()`. `row.getToggleSelectedHandler()` given the click event adds Shift-click ranges.
- Bulk actions use `table.getSelectedRowIds()`, which survives server pages (`getSelectedRowModel()` holds only loaded rows). Show the count, confirm destructive actions, disable while pending, then `table.resetRowSelection(true)`.

## Long tables, loading and empty states

- Paginate by default. When one view must render more than a few hundred rows, virtualize with TanStack Virtual: count `table.getRowModel().rows` (never raw `data`), key by `row.id`, scroll a fixed-height container, measure with `measureElement` and `data-index`, and position rows (`display: grid` on `tbody` and rows with `translateY`, or spacer rows). Keep the real `<table>` with a sticky header, `aria-rowcount` on the table and `aria-rowindex` on rows, and widths from `column.getSize()` (`columnSizingFeature`).
- First load: skeleton rows matching the columns and row height. Refetch: keep the rows, dim them and set `aria-busy="true"`; never blank the table between pages.
- Empty: one row spanning the visible columns that tells "nothing yet" from "no matches" and offers to clear filters. Errors offer a retry.

## Accessibility

- Use real table elements through the Table parts, never a grid of divs, and name the table (`aria-label` or a caption).
- Header cells get `scope="col"`. Only the sorted column carries `aria-sort` (`"ascending"` or `"descending"`).
- Label every checkbox ("Select all rows on this page", `` `Select ${row.original.email}` ``) and every icon-only button.
- Don't make whole rows clickable without a keyboard path: put a link or button in the primary cell.
- Make a sideways-scrolling container focusable and named; announce result counts after filtering in a polite live region.

## Do not

- Do not write v8 APIs: `useReactTable`, `getCoreRowModel: getCoreRowModel()` and the other `get*RowModel()` options, `sortingFn`/`sortingFns`, `table.getState()`, `onStateChange`, `VisibilityState` (now `ColumnVisibilityState`), `columnSizingInfo` (now `columnResizing`), pinning `left`/`right` (now `start`/`end`), table-level `enablePinning`.
- Do not use `useLegacyTable` from `@tanstack/react-table/legacy` or `stockFeatures` in new code: migration bridges that bundle every feature.
- Do not cast to reach a missing method: register its feature.
- Do not number rows or build ids from `row.index`; use `getRowId` and `row.getDisplayIndex()`.

## Before you finish

- Run the type checker; add no `any` or casts to silence feature types.
- Sort each sortable column both ways, filter to zero rows, page to the end and back, toggle columns, select across pages and run a bulk action.
- In server mode, check that the URL holds the state and that reload and the back button restore it.
- Tab through sort buttons, checkboxes and row actions; check names and `aria-sort` with a screen reader. Try 1,000 rows and the loading, empty and error states.

11,489 characters, 1,501 words. Under Windsurf's 12,000-character limit for a rule, as every prompt here is.

Use it in your agent

The same rules, where your agent reads them. Each file has the front matter its agent needs, set to load when the task fits.

Claude Code

A rule it loads every session

  1. Save it as .claude/rules/react-tables.md in your project, front matter included, or run the install command.
  2. Its paths front matter loads it when Claude reads or edits a matching file, and keeps it out of other work.
  3. Prefer one file? Paste it into CLAUDE.md under its own heading instead.

.claude/rules/react-tables.md

---paths:  - "**/*table*.tsx"  - "**/columns.tsx"---# React TablesYou are working in a React 19 project with TypeScript, TanStack Table v9 (`@tanstack/react-table`) and shadcn/ui-style Table parts (`Table`, `TableHeader`, `TableRow`, `TableHead`, `TableCell`…). Write code for the versions in `package.json`: v9 changed how a table is built, how state is read and several names, and most examples you remember are v8.## Before you write code- Check the major version of `@tanstack/react-table`. These rules are for v9; on a v8 project follow v8's docs and never mix the two APIs.- v9 ships its own guides: read `node_modules/@tanstack/react-table/skills/` (getting-started, table-state, migrate-v8-to-v9) and the feature you are adding in `node_modules/@tanstack/table-core/skills/table-features/references/`. Exact signatures: `node_modules/@tanstack/table-core/dist/features/<feature>/*.types.d.ts`.- Reuse the project's Table parts (`components/ui/table.tsx` or similar) and the shape of its existing data tables.## Build the table…

Or let the shadcn CLI write this file: sign in (free) for your command.

Cursor

A project rule with front matter

  1. Save it as .cursor/rules/react-tables.mdc, front matter included, or run the install command.
  2. Cursor attaches it when you work on matching files, or when the task fits its description.
  3. Check it under Cursor Settings, Rules, then commit the folder for your team.

.cursor/rules/react-tables.mdc

---description: "Data tables with TanStack Table v9: columns, features, sorting, filtering, pagination, selection, virtualization and table accessibility."globs: **/*table*.tsx,**/columns.tsxalwaysApply: false---# React TablesYou are working in a React 19 project with TypeScript, TanStack Table v9 (`@tanstack/react-table`) and shadcn/ui-style Table parts (`Table`, `TableHeader`, `TableRow`, `TableHead`, `TableCell`…). Write code for the versions in `package.json`: v9 changed how a table is built, how state is read and several names, and most examples you remember are v8.## Before you write code- Check the major version of `@tanstack/react-table`. These rules are for v9; on a v8 project follow v8's docs and never mix the two APIs.- v9 ships its own guides: read `node_modules/@tanstack/react-table/skills/` (getting-started, table-state, migrate-v8-to-v9) and the feature you are adding in `node_modules/@tanstack/table-core/skills/table-features/references/`. Exact signatures: `node_modules/@tanstack/table-core/dist/features/<feature>/*.types.d.ts`.- Reuse the project's Table parts (`components/ui/table.tsx` or similar) and the shape of its existing data tables.## Build the table…

Or let the shadcn CLI write this file: sign in (free) for your command.

Windsurf

A workspace rule for Cascade

  1. Save it as .windsurf/rules/react-tables.md, or run the install command.
  2. trigger: model_decision lets Cascade pull it in when the task fits its description.
  3. Windsurf reads up to 12,000 characters per rule; this one is 11,489.

.windsurf/rules/react-tables.md

---trigger: model_decisiondescription: "Data tables with TanStack Table v9: columns, features, sorting, filtering, pagination, selection, virtualization and table accessibility."---# React TablesYou are working in a React 19 project with TypeScript, TanStack Table v9 (`@tanstack/react-table`) and shadcn/ui-style Table parts (`Table`, `TableHeader`, `TableRow`, `TableHead`, `TableCell`…). Write code for the versions in `package.json`: v9 changed how a table is built, how state is read and several names, and most examples you remember are v8.## Before you write code- Check the major version of `@tanstack/react-table`. These rules are for v9; on a v8 project follow v8's docs and never mix the two APIs.- v9 ships its own guides: read `node_modules/@tanstack/react-table/skills/` (getting-started, table-state, migrate-v8-to-v9) and the feature you are adding in `node_modules/@tanstack/table-core/skills/table-features/references/`. Exact signatures: `node_modules/@tanstack/table-core/dist/features/<feature>/*.types.d.ts`.- Reuse the project's Table parts (`components/ui/table.tsx` or similar) and the shape of its existing data tables.## Build the table…

Or let the shadcn CLI write this file: sign in (free) for your command.

GitHub Copilot

Instructions for the repository

  1. Save it as .github/instructions/react-tables.instructions.md, or run the install command.
  2. applyTo limits it to the files it's about, in chat, the coding agent and code review.
  3. Or paste it into .github/copilot-instructions.md, which Copilot reads for every request.

.github/instructions/react-tables.instructions.md

---description: "Data tables with TanStack Table v9: columns, features, sorting, filtering, pagination, selection, virtualization and table accessibility."applyTo: "**/*table*.tsx,**/columns.tsx"---# React TablesYou are working in a React 19 project with TypeScript, TanStack Table v9 (`@tanstack/react-table`) and shadcn/ui-style Table parts (`Table`, `TableHeader`, `TableRow`, `TableHead`, `TableCell`…). Write code for the versions in `package.json`: v9 changed how a table is built, how state is read and several names, and most examples you remember are v8.## Before you write code- Check the major version of `@tanstack/react-table`. These rules are for v9; on a v8 project follow v8's docs and never mix the two APIs.- v9 ships its own guides: read `node_modules/@tanstack/react-table/skills/` (getting-started, table-state, migrate-v8-to-v9) and the feature you are adding in `node_modules/@tanstack/table-core/skills/table-features/references/`. Exact signatures: `node_modules/@tanstack/table-core/dist/features/<feature>/*.types.d.ts`.- Reuse the project's Table parts (`components/ui/table.tsx` or similar) and the shape of its existing data tables.## Build the table…

Or let the shadcn CLI write this file: sign in (free) for your command.

Codex & AGENTS.md

The file most agents read

  1. Paste it into AGENTS.md at your project's root, under its own heading.
  2. Codex reads AGENTS.md before it starts work, and so do Cursor, Zed, Jules, Amp and Copilot's coding agent.
  3. A folder can have its own AGENTS.md: the one closest to the file being edited wins.

AGENTS.md

## React TablesYou are working in a React 19 project with TypeScript, TanStack Table v9 (`@tanstack/react-table`) and shadcn/ui-style Table parts (`Table`, `TableHeader`, `TableRow`, `TableHead`, `TableCell`…). Write code for the versions in `package.json`: v9 changed how a table is built, how state is read and several names, and most examples you remember are v8.### Before you write code- Check the major version of `@tanstack/react-table`. These rules are for v9; on a v8 project follow v8's docs and never mix the two APIs.- v9 ships its own guides: read `node_modules/@tanstack/react-table/skills/` (getting-started, table-state, migrate-v8-to-v9) and the feature you are adding in `node_modules/@tanstack/table-core/skills/table-features/references/`. Exact signatures: `node_modules/@tanstack/table-core/dist/features/<feature>/*.types.d.ts`.- Reuse the project's Table parts (`components/ui/table.tsx` or similar) and the shape of its existing data tables.### Build the table…

AGENTS.md is shared with your own instructions, so paste the rules in: the copy sets their headings a level down.

Rules teach it. The MCP equips it. With Pro, your agent installs every component, block and icon it needs, from a prompt.

Get Pro

Pricing

Give your agent the parts, with Pro

The prompts are free. Every Pro plan adds the MCP server, so your agent installs the components, blocks and icons it writes code around.

4.9/5 out of 5 · 150+ reviewsfrom 150+ founders and developers.

Billed once a year in USD. Cancel anytime.

Individual

For personal projects & open source

$32instead of $49/mo

$389 billed yearly

  • How many people can use the license.Seats: 1 seat
  • Client work, SaaS products and anything you charge for.Commercial use: Personal & open source only
  • Page sections in 56 categories, from heroes and pricing tables to dashboards and settings, on every framework and UI library, in your theme.Blocks: All 6,181 blocks
  • Complete Next.js sites for startups, SaaS, agencies and portfolios, every page designed, ready to ship.Templates: All 14 templates
  • Your AI agent searches, previews and installs any component, block, icon or DESIGN.md from a prompt: 22 tools in Claude Code, Cursor, VS Code and 35 more clients.MCP server: 38 AI clients
  • Buttons with motion and states of their own, from an arrow that slides in to a burst of confetti.Animated buttons: All 35 buttons
  • Typewriters, scrambles, counters and more, for headlines, labels and numbers.Text effects: All 97 effects
  • Animated backgrounds for heroes, sections and cards, from a soft aurora to a flock that flees the cursor. Most are pure CSS.Backgrounds: All 100 backgrounds
  • WebGPU shaders, from a slow aurora to a black hole, each drawing only while it's on screen.Shaders: All 100 shaders
  • One design and one theme on all four: the components, blocks and effects are built for each framework.Frameworks: React, Vue, Svelte, Solid
  • React on Base UI, Radix UI, React Aria or Ark UI: install the version for the library your project already uses. Vue, Svelte and Solid run on Ark UI.UI libraries: Base UI, Radix, Aria, Ark
  • From a button to a date picker, a color picker or a guided tour: accessible, typed in TypeScript, each with live demos.Components: All 174 components
  • Real-world patterns built from the components, in 53 galleries: forms, tables, menus, dialogs and more.Examples: All 1,103 examples
  • Area, bar, line, pie, radar and radial charts on TanStack Charts, on every framework. React also gets the Recharts originals.Charts: All 70 charts
  • Debounce, local storage, media queries and more: each hook is one typed file you own.React hooks: All 35 hooks
  • From 222 libraries, Lucide, Heroicons, Tabler and Material Symbols among them, as SVG or a React component in one command.Icons: 294k+ icons
  • Stripe, Linear, Apple and 497 more: a brand's colors, typography and components in one file your AI agent builds from.DESIGN.md files: All 500 brands
  • Restyle every component and block at once: 8 styles, 7 grays, 18 colors and 5 radii, shared as a shadcn preset code.Theme builder: 8 styles × 18 colors
  • Priority requests are answered first.Support: Standard

Team

Most popular

For freelancers, agencies & startups

$66instead of $99/mo

$789 billed yearly · ≈ $13/dev/mo

Everything in Individual, plus

  • How many people can use the license.Seats: Up to 5 seats
  • Client work, SaaS products and anything you charge for.Commercial use: Included
  • Priority requests are answered first.Support: Priority

Organization

For companies with larger teams

$132instead of $199/mo

$1,589 billed yearly

Everything in Team, plus

  • How many people can use the license.Seats: Unlimited seats
  • Secure checkout via Stripe
  • Code lives in your repo
  • Keep what you've shipped if you cancel
  • License terms

React Tables prompt FAQ

What the React Tables prompt covers and how to use it in your agent.

Does the React Tables prompt work with Base UI, Radix and React Aria components?

It is written for shadcn/ui-style Table parts, which render plain table elements, so the inputs and menus around them can come from Base UI, Radix or React Aria. Checkbox props differ, and the prompt names Base UI's indeterminate prop and Radix's indeterminate checked value. React Aria's own Table component has its own sorting and selection model and is outside this prompt.

What makes a table accessible under the React Tables prompt?

Real table elements with a name, header cells with scope, sort controls as buttons inside the header with aria-sort on the sorted column only, and a label on every checkbox and icon button. Refetches set aria-busy, and result counts after filtering are announced in a polite live region.

4.9 out of 5 from 150+ developers

Rules teach it. The MCP equips it.

Every component, block and icon, installed by your AI agent on your stack. The prompts stay free.

Get Pro
  • Cancel anytime
  • Keep what you've shipped