# React Tablekit: the complete documentation > A React data table with a framework-agnostic core. Pagination, search, filters, sorting, grouping, expansion, selection and faceting each run in the browser or on your server. Every part is a replaceable slot, every interaction is overridable, and every visual value is a CSS variable. Version 1.0.2. Every page of https://react-tablekit.vercel.app/react-tablekit/ in reading order, generated from the same sources in the same build as the site. Each live example is written out as its source and each reference page as its tables. The examples import a few site modules (paths beginning `@/`); they are at the end, under "Shared example code". Every page begins with its title as a level-one heading and its URL, so an answer taken from this file can name the page it came from. ## Contents - **Getting started**: Overview, Installation, Quickstart, AI context, Requirements and compatibility, FAQ, Support, Versions - **Features**: All features, Columns, Sorting, Global search, Column filters, Faceting, Pagination, Row selection, Expansion, Client and server, Tree data, Grouping, Column pinning, Column sizing, Column ordering, Column visibility, Column footers, Row pinning, Row numbers, Row appearance, Sticky header and footer, Surface, Density, Virtualization, Responsive, Toolbar, Loading and empty states, Keyboard navigation, Export, Persistence, Headless, Localization - **Demos**: Demos, Account list, Asset list, Reading history, Asset picker, Playground, Theme editor - **Reference**: API reference, DataTableProps, ColumnDef, TableInstance, TableState, Hooks, Utilities, TableSlots, Handlers, Theme tokens, Localization keys, Icons - **Customization**: How to customize, Theming, Design tokens, Overriding structure, Slots, Rows and cells, Cell building blocks, Handler middleware, Recipes - **Guides**: Guides, Server data, Best practices, Performance, Testing, Accessibility, Security - **Integrations**: Integrations, React Query, React Router, Next.js, Tailwind CSS, i18next - **Migration**: Migration - **Discover more**: Discover more, Roadmap, Changelog --- # Overview > A headless-core React data table where every feature works on the client and on the server. URL: https://react-tablekit.vercel.app/react-tablekit/ ## Introduction react-tablekit renders tabular data in React. The engine is framework-agnostic and renders nothing on its own; the components on top of it are a default you can replace part by part. Its organising idea is that **where the work happens should not change your code**. Pagination, global search, column filters, sorting, grouping, expansion, selection and faceting each run either in the browser or on your server, chosen per feature. Starting client-side and moving to the server later is a prop, not a rewrite. What it is not: a spreadsheet, a chart library, or a data-fetching library. It has no opinion about how rows reach it: `fetch`, React Query, a router loader or an array in memory all work. ## Why react-tablekit - **Client and server are the same code.** Each feature picks its side; the table handles the debouncing, aborting, race conditions and out-of-range pages that make server mode tedious. - **Nothing is sealed.** Every part is a slot, every interaction is middleware you can wrap, and every visual value is a CSS variable. - **Headless underneath.** `useDataTable` is the whole engine with no markup; `createTable` runs without React at all. - **Accessible by default.** Correct table semantics, the right ARIA role for the features you enable, the data-grid keyboard pattern and polite live announcements. - **Zero runtime dependencies.** Only React as a peer. ESM and CJS with types for both, tree shakeable and server-render safe. ## Start now - [Installation](/react-tablekit/getting-started/installation/): install the package and the stylesheet. - [Quickstart](/react-tablekit/getting-started/quickstart/): a complete first table, nothing elided. - [All features](/react-tablekit/all-features/): every capability, each with a live demo. - [Client and server](/react-tablekit/client-and-server/): the defining feature, in one page. - [Customization](/react-tablekit/customization/): the nine levels, from a token to the core. - [API reference](/react-tablekit/api/): generated from the source, so it cannot drift. ## Licence MIT, with no paid tier and no feature gating. Everything documented on this site is in the package you install. --- # Installation > Install the package, import the stylesheet and render your first table. URL: https://react-tablekit.vercel.app/react-tablekit/getting-started/installation/ ## Prerequisites - **React 18.2 or newer** (React 18 and 19 are both supported), with `react-dom` at the same version. - **TypeScript 5.4 or newer** if you use TypeScript. The package ships its own types; nothing extra to install. - A bundler that understands ES modules: Vite, Next.js, Remix, Rspack, webpack 5 or Parcel. ## Install ```sh pnpm add @kingtech/react-tablekit ``` ```sh npm install @kingtech/react-tablekit ``` ```sh yarn add @kingtech/react-tablekit ``` ## Import the stylesheet Import it once, wherever your application starts. ```ts import '@kingtech/react-tablekit/styles.css'; ``` The styles live inside `@layer tablekit`, so any rule you write outside that layer wins without `!important`. ## Verify Render the table from the [quickstart](/react-tablekit/getting-started/quickstart/). You should see a table with sortable headers, a search box above it and pagination below it. If the table renders but has no borders or spacing, the stylesheet import is missing. ## Next steps - [Quickstart](/react-tablekit/getting-started/quickstart/): a complete, copy-pasteable first table. - [Columns](/react-tablekit/columns/): how a column is defined. - [Client and server](/react-tablekit/client-and-server/): moving the work to your API. --- # Quickstart > A complete first table: data, columns and the features that are on by default. URL: https://react-tablekit.vercel.app/react-tablekit/getting-started/quickstart/ ## Prerequisites The package installed and `@kingtech/react-tablekit/styles.css` imported; see [Installation](/react-tablekit/getting-started/installation/). ## A complete table This is the whole file. Nothing is elided. **Example: A minimal table** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/columns/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(120); /** The minimal table: `data` + `columns` (plus an accessible name and stable row ids). */ export default function BasicExample() { return ( p.id} /> ); } ``` ```tsx import '@kingtech/react-tablekit/styles.css'; import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; interface Person { id: string; firstName: string; lastName: string; email: string; age: number; } const people: Person[] = [ { id: '1', firstName: 'Ada', lastName: 'Lovelace', email: 'ada@example.com', age: 36 }, { id: '2', firstName: 'Grace', lastName: 'Hopper', email: 'grace@example.com', age: 45 }, ]; const col = createColumnHelper(); const columns = [ col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name' }), col.accessor('email', { header: 'Email' }), col.accessor('age', { header: 'Age', type: 'number' }), ]; export function People() { return ( person.id} /> ); } ``` ## Verify You should now see a table with three columns. Click a header to sort it, type in the search box to filter, and use the toolbar to hide a column. None of that needed configuration: sorting, global search, column filters, column visibility, density and pagination are on by default. ## What you did not have to write `getRowId` is the only prop that looks like bookkeeping, and it earns its place: stable row identity is what lets selection survive a refetch, expansion survive a sort, and "select all matching" work across server pages. ## Next steps - [All features](/react-tablekit/all-features/): everything the table can do. - [Sorting](/react-tablekit/sorting/): comparators, multi-sort and server sorting. - [Client and server](/react-tablekit/client-and-server/): one prop moves the work to your API. --- # AI context > The whole documentation as one Markdown file, with the source of every example, for coding agents. URL: https://react-tablekit.vercel.app/react-tablekit/getting-started/ai-context/ A coding agent writes better table code from this documentation than from what it remembers about data tables in general. So the site also publishes itself as Markdown: one file with every page and every example, an index of the pages, and each page on its own. ## Prerequisites - A coding agent that can read files in your repository or fetch a URL: Claude Code, Cursor, GitHub Copilot, Codex or similar. - `@kingtech/react-tablekit` installed, or about to be. See [Installation](/react-tablekit/getting-started/installation/). ## Installation Save the full file next to your code, where the agent can read it: ```sh curl -o docs/react-tablekit.md https://react-tablekit.vercel.app/llms-full.md ``` Then tell the agent it is there. One line in the instructions file your tool reads at the start of every session is enough: `CLAUDE.md`, `AGENTS.md`, a rule in `.cursor/rules/`, or `.github/copilot-instructions.md`. ```md For anything involving data tables, read docs/react-tablekit.md first, and prefer it to what you remember. ``` The file describes one version of the package. When you upgrade, download it again. ## What is in the file Every page of this documentation in reading order: the getting-started pages, every feature page, the demos, the guides and the whole API reference. Where the site shows a running example, the file has its source; where the site shows a reference table, the file has the table. The examples import a few site modules (sample data, shared columns and the mocked API they fetch from), and those are included once at the end, so every example is complete. Each page starts with its title and its URL, so an agent can say which page an answer came from. The file is generated from the same Markdown as the site, in the same build, so it cannot say anything the site does not. ## Which file to use | File | What it holds | Use it when | | ------------------------------- | ------------------------------------------------------ | ----------------------------------------- | | [llms-full.md](/llms-full.md) | Every page and the source of every example | the agent should know the whole library | | [llms-full.txt](/llms-full.txt) | The same file, under the name tools look for | a tool asks for a documentation URL | | [llms.txt](/llms.txt) | An index: one line per page, each linking its Markdown | the agent should fetch only what it needs | | A page's URL + `index.md` | That one page, with its examples | you are working on a single feature | The full file is large, but it fits whole in the context window of a current frontier model. If your tool's window is smaller, give it `llms.txt` and let it fetch the pages it needs. Any page works on its own the same way: [the sorting page](/react-tablekit/sorting/index.md) is `/react-tablekit/sorting/` with `index.md` added. ## Minimal working example With the file in `docs/`, a request like this has everything it needs to be answered from the documentation rather than from guesswork: ```text Using docs/react-tablekit.md, add an orders table to OrdersPage. Load it from GET /api/orders with server-side paging and sorting, filter by status (open, paid, refunded), and let the user export the current page as CSV. ``` ## Verify Ask the agent something only the documentation answers: for example, which CSS variable sets the background of striped rows. An agent reading the file answers `--tk-row-striped-bg` and can name the page it came from, Theme tokens. One that is not reading it guesses. ## Next steps - [Installation](/react-tablekit/getting-started/installation/): the package, its peers and the stylesheet. - [Quickstart](/react-tablekit/getting-started/quickstart/): a complete first table. - [API reference](/react-tablekit/api/): generated from the TypeScript declarations. --- # Requirements and compatibility > Supported React versions, browsers, bundlers and rendering environments. URL: https://react-tablekit.vercel.app/react-tablekit/getting-started/requirements/ ## React | Requirement | Version | | ----------- | ----------------------------- | | `react` | >= 18.2 (18 and 19 supported) | | `react-dom` | >= 18.2 | Both are peer dependencies. The package itself has **zero runtime dependencies**. ## Browsers The last two versions of evergreen browsers, and Safari 15.4 or newer. The baseline features are `@layer` and `inset`; `:has()` is used only as an enhancement. ## Bundlers and frameworks The package ships ESM and CJS with type definitions for both, and declares `sideEffects` so bundlers can tree-shake it. The React entry carries a `"use client"` banner, so it works in the Next.js App Router without extra wrapping. See [Next.js](/react-tablekit/integrations/next-js/). ## Server rendering The table renders on the server: rows, `role="grid"` and the cards layout are all present in the server HTML. Because there is no viewport to measure, responsive values need a declared breakpoint; see `ssrBreakpoint` in [Responsive](/react-tablekit/responsive/). ## Bundle size | Entry | min+gzip | | ------------------------------------------------------ | -------- | | `import { DataTable } from '@kingtech/react-tablekit'` | 56.5 kB | | Full import | 60.1 kB | | `@kingtech/react-tablekit/core` | 20.8 kB | | `@kingtech/react-tablekit/styles.css` | 6.1 kB | `DataTable` is batteries-included, so the first two numbers are close together. If you need a small bundle, use [`@kingtech/react-tablekit/core`](/react-tablekit/headless/) with your own markup. --- # FAQ > Short answers about licensing, scope, styling and how this library compares. URL: https://react-tablekit.vercel.app/react-tablekit/getting-started/faq/ ## Is it free? Yes. MIT licensed, with no paid tier, no feature gating and no licence key. You can use it commercially. ## Does it depend on a component framework? No. There is no Material UI, no styled-components, no CSS-in-JS runtime. Styling is plain CSS with custom properties inside `@layer tablekit`. ## Can I use my own design system? Yes, at three levels: override CSS variables, pass your own class names, or replace whole parts with your own components. See [How to customize](/react-tablekit/customization/). ## Does it do server-side data? Every data feature does, and each one independently. See [Client and server](/react-tablekit/client-and-server/). ## Can I use it without the built-in markup? Yes. `useDataTable` gives you the engine and renders nothing; `createTable` runs without React at all. See [Headless](/react-tablekit/headless/). ## Is inline editing supported? Not in 1.0. It is specified and planned; see the [roadmap](/react-tablekit/discover-more/roadmap/). ## How large is it? 56.5 kB min+gzip for a basic import; 20.8 kB for the headless core. Full numbers in [Requirements](/react-tablekit/getting-started/requirements/). --- # Support > Where to ask a question, report a bug and request a feature. URL: https://react-tablekit.vercel.app/react-tablekit/getting-started/support/ ## Reporting a bug [Open an issue on GitHub](https://github.com/kiralygyula92/react-tablekit/issues). A reproduction is worth more than a description: the fastest route is a link to a [playground](/react-tablekit/demos/playground/) configuration that shows the problem, since the whole configuration lives in the URL. Please include the version, the React version and whether the table is in client or server mode. ## Asking a question Use GitHub issues for questions too. There is no paid support channel and no guaranteed response time; this is a free, MIT-licensed project. ## Requesting a feature Check the [roadmap](/react-tablekit/discover-more/roadmap/) first: several capabilities are specified but deliberately deferred past 1.0. If it is not there, open an issue describing the problem you are solving rather than the API you imagine. ## Security Report a suspected vulnerability privately through [GitHub's security advisories](https://github.com/kiralygyula92/react-tablekit/security/advisories) rather than a public issue. See the [security guide](/react-tablekit/guides/security/) for the threat model. --- # Versions > Which versions are supported, and what a minor release is allowed to change. URL: https://react-tablekit.vercel.app/react-tablekit/getting-started/versions/ ## Current version **1.0.0**: the first stable release. | Version | Status | Documentation | | ------- | --------- | ------------- | | v1 | Supported | This site | ## What the public API covers More than the exported names. All of the following are public, and changing or removing any of them is a **major** release: - every name and signature in the [API reference](/react-tablekit/api/); - the `tk-*` class names, so a stylesheet that targets them keeps working; - the `--tk-*` CSS variables; - the `data-*` attributes the table puts on its elements; - slot names, handler names and localization keys. ## What a minor release may change Token default _values_. A minor may retune a colour or a spacing step. If you depend on an exact value, set it yourself rather than relying on the default. The `classic` preset is the exception: its values are frozen, because its whole purpose is to look exactly the way it looks. ## Deprecations A deprecated API is marked `@deprecated` in its TSDoc, warns once per session in development builds, and is removed in the next major release at the earliest. ## Pre-releases Pre-release versions are published under the `next` dist-tag, so `pnpm add @kingtech/react-tablekit` always installs a stable version. --- # All features > Every capability the table ships with, grouped the way the sidebar groups them. URL: https://react-tablekit.vercel.app/react-tablekit/all-features/ Each capability below is one page with a runnable demo, its variations, the ways it can be customised, its known limitations and links to the generated reference. **What is in scope:** rendering, querying and interacting with tabular data, in the browser or against a server. **What is deliberately out:** fetching (bring your own), charting, pivot tables and spreadsheet-style editing. Inline editing is specified and planned; see the [roadmap](/react-tablekit/discover-more/roadmap/). ## Core features - [Columns](https://react-tablekit.vercel.app/react-tablekit/columns/): Accessors, headers, types and formatting: the column definition is where most of a table is decided. - [Sorting](https://react-tablekit.vercel.app/react-tablekit/sorting/): Click to sort, shift-click to add a column, and comparators that understand text, dates and your own domain order. - [Global search](https://react-tablekit.vercel.app/react-tablekit/global-search/): One input that matches across every searchable column, with debouncing, a minimum length and optional match highlighting. - [Column filters](https://react-tablekit.vercel.app/react-tablekit/column-filters/): Per-column filters in a panel, a popover or a filter row, with a control chosen from the column's type and values drawn from the data. - [Faceting](https://react-tablekit.vercel.app/react-tablekit/faceting/): Filter options and counts computed from the data: from the loaded rows in client mode, from your API in server mode. - [Pagination](https://react-tablekit.vercel.app/react-tablekit/pagination/): Six presentations of the same state (numbered, compact, simple, load-more, infinite or none), including cursor-based paging. - [Row selection](https://react-tablekit.vercel.app/react-tablekit/row-selection/): Checkboxes or radios, range selection with shift-click, a selection bar for bulk actions, and select-all that survives server paging. - [Expansion](https://react-tablekit.vercel.app/react-tablekit/expansion/): Detail panels that open under a row, lazily rendered, animated, and pinned to the viewport width so a wide table cannot scroll them away. - [Client and server](https://react-tablekit.vercel.app/react-tablekit/client-and-server/): The same table and the same props, with the work done in the browser, on your API, or split feature by feature. ## Advanced features - [Tree data](https://react-tablekit.vercel.app/react-tablekit/tree-data/): Hierarchical rows with indentation, expand toggles and children that can be loaded from the server when a branch is opened. - [Grouping](https://react-tablekit.vercel.app/react-tablekit/grouping/): Group rows by one column or several, with aggregates in the group row and totals in the footer. ## Display & layout - [Column pinning](https://react-tablekit.vercel.app/react-tablekit/column-pinning/): Freeze columns to either edge, stacked and responsive, with a shadow that appears only when content actually scrolls underneath. - [Column sizing](https://react-tablekit.vercel.app/react-tablekit/column-sizing/): Percentage or pixel widths, a keyboard-operable resize handle, and double-click autosizing that measures the rendered rows. - [Column ordering](https://react-tablekit.vercel.app/react-tablekit/column-ordering/): Drag headers to reorder, or move them from the columns menu with the keyboard. Pinned groups and locked positions are respected. - [Column visibility](https://react-tablekit.vercel.app/react-tablekit/column-visibility/): A columns menu for the reader, a per-column menu for actions, and responsive hiding that needs no state at all. - [Column footers](https://react-tablekit.vercel.app/react-tablekit/column-footers/): A footer row with per-column totals, computed over the current page or over everything the filters left. - [Row pinning](https://react-tablekit.vercel.app/react-tablekit/row-pinning/): Keep chosen rows at the top or the bottom while the rest of the table sorts, filters and pages away beneath them. - [Row numbers](https://react-tablekit.vercel.app/react-tablekit/row-numbers/): A numbered column that counts through the whole result set, or restarts on every page. - [Row appearance](https://react-tablekit.vercel.app/react-tablekit/row-appearance/): Striping, hover and per-row classes, styles and props: the way a row shows what it is without a custom renderer. - [Sticky header and footer](https://react-tablekit.vercel.app/react-tablekit/sticky-header-and-footer/): Keep the header and the totals in view while the rows scroll, in both axes when columns are pinned. - [Surface](https://react-tablekit.vercel.app/react-tablekit/surface/): The panel the table sits on: a bordered card with its pagination attached, or nothing at all, with corners you choose. - [Density](https://react-tablekit.vercel.app/react-tablekit/density/): Three row heights the reader can switch between (compact, standard and comfortable), driven entirely by tokens. - [Virtualization](https://react-tablekit.vercel.app/react-tablekit/virtualization/): Render the rows on screen instead of all of them: a hundred thousand rows with the same markup, the same keyboard model and the same counts for screen readers. - [Responsive](https://react-tablekit.vercel.app/react-tablekit/responsive/): Breakpoint-aware props, a card layout for small screens, and a server-rendered first paint that does not jump. ## Interaction - [Toolbar](https://react-tablekit.vercel.app/react-tablekit/toolbar/): The strip above the table (search, filters, columns, density, export), either as one component or as parts you place yourself. - [Loading and empty states](https://react-tablekit.vercel.app/react-tablekit/loading-and-empty-states/): Skeletons, a delayed overlay, an empty state that knows why it is empty, and errors that keep the rest of the table usable. - [Keyboard navigation](https://react-tablekit.vercel.app/react-tablekit/keyboard-navigation/): The WAI-ARIA data grid pattern: arrow keys between cells, one tab stop for the whole table, and Enter to reach interactive content. ## Automation - [Export](https://react-tablekit.vercel.app/react-tablekit/export/): CSV of the page, the selection or everything matching, chunked through your API when the result is larger than the browser holds. - [Persistence](https://react-tablekit.vercel.app/react-tablekit/persistence/): Put table state in the URL, in storage, or in both, so a link reproduces a view and a reload does not lose one. ## Developer tools - [Headless](https://react-tablekit.vercel.app/react-tablekit/headless/): The engine without the markup: every feature, none of the table. Use it for cards, lists, charts, or a table you draw yourself. - [Localization](https://react-tablekit.vercel.app/react-tablekit/localization/): Every string the table can show, in one object, with number, date and currency formatting that follows the same locale. --- # Columns > Accessors, headers, types and formatting: the column definition is where most of a table is decided. URL: https://react-tablekit.vercel.app/react-tablekit/columns/ ## Basics **Example: Columns** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/columns/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(120); /** The minimal table: `data` + `columns` (plus an accessible name and stable row ids). */ export default function BasicExample() { return ( p.id} /> ); } ``` A column says where its value comes from and what it looks like. `createColumnHelper()` gives you an `accessor` that keeps the value type attached, so `cell` and `sortingFn` are typed against the real field rather than `unknown`. ```tsx const col = createColumnHelper(); const columns = [ col.accessor('firstName', { header: 'First name' }), col.accessor((p) => p.firstName + ' ' + p.lastName, { id: 'name', header: 'Name' }), col.display({ id: 'actions', header: '', cell: ({ row }) => }), ]; ``` An **accessor column** reads a value and can therefore sort, filter, group and export. A **display column** has no value: it renders something, and the table leaves it out of sorting, filtering and CSV. A **group column** has `columns` of its own and produces a second header row. ### Column types **Example: Column types** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/columns/demo-types) ```tsx import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const data = generatePeople(50); const col = createColumnHelper(); const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }); const date = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }); /** * `type` is the one setting that moves several defaults at once: alignment, the comparator, * the filter control and the formatter. Everything it picks can still be overridden per column. */ const columns = [ col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name', type: 'text', getSearchValue: (p) => `${p.firstName} ${p.lastName} ${p.email}`, }), // Numbers align right and sort numerically without being told to. col.accessor('age', { header: 'Age', type: 'number' }), col.accessor('salary', { header: 'Salary', type: 'number', format: (v) => money.format(v), }), // Dates sort chronologically; `format` decides only what is displayed, searched and exported. col.accessor('joined', { header: 'Joined', type: 'date', format: (v) => date.format(new Date(v)), }), col.accessor('remote', { header: 'Remote', type: 'boolean', align: 'center', format: (v) => (v ? 'Yes' : 'No'), }), // `city` is absent for roughly one row in six; that is what the fallback is for. col.accessor('city', { header: 'City', renderFallbackValue: unknown, }), // A display column has no value of its own: it is computed, so it cannot be sorted or filtered. col.display({ id: 'tenure', header: 'Tenure', align: 'right', cell: ({ row }) => { const years = Math.max( 0, new Date().getFullYear() - new Date(row.original.joined).getFullYear(), ); return years === 0 ? 'First year' : `${String(years)} yr`; }, }), ]; /** Column types, formatting, alignment and fallback values. */ export default function ColumnTypesExample() { return ( aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} enableColumnFilters initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` `type` sets the defaults a column would otherwise repeat: alignment, formatting, the comparator and the filter control. `'number'` right-aligns and filters by range, `'date'` formats through the locale and filters by date, `'boolean'` renders a yes/no and filters tri-state. ```tsx col.accessor('total', { type: 'currency', meta: { currency: 'EUR' } }); ``` Anything you set explicitly still wins; the type only supplies what you left out. ### Formatting a value `format(value, row)` changes what is displayed while sorting, filtering, searching and export keep the underlying value, which is why a formatted date still sorts chronologically. Use `cell` when you need markup rather than text. ```tsx col.accessor('createdAt', { type: 'date', format: (v) => relativeTime(v) }); col.accessor('status', { cell: ({ getValue }) => }); ``` For empty values, `renderFallbackValue` replaces `null`, `undefined` and the empty string in one place instead of a guard in every cell. ### Widths and truncation `width`, `minWidth` and `maxWidth` are CSS widths and accept percentages; `size`, `minSize` and `maxSize` are the pixel model used once [column resizing](/react-tablekit/column-sizing/) is on. `truncate` clips with an ellipsis and keeps the full text as a tooltip, and `noWrap` stops a long cell wrapping to a second line. ## Customization `columnDefaults` applies a partial definition to every column, so a table-wide convention is written once: ```tsx ``` `defineColumns` is the same list without the helper's binding, useful when the columns are assembled somewhere the row type is not in scope yet. Header and cell rendering are replaceable through [slots](/react-tablekit/customization/slots/). ## Limitations - **A column's `id` must be unique and stable.** An accessor function column has no natural id, so one must be given; changing ids between renders resets sizing, ordering and visibility. - **Display columns never appear in CSV.** They have no value to export. If a computed column should be exported, make it an accessor column with `accessorFn` and an `exportValue`. - **Group columns cannot be sorted or filtered themselves.** They are header containers; their leaves carry the behaviour. ## API - [`ColumnDef`](/react-tablekit/api/column-def/): the whole definition. - [`DataTableProps`](/react-tablekit/api/data-table-props/): `columns`, `columnDefaults`, `defaultColumn`, `displayColumnDefs`, `renderFallbackValue`. - [Utilities](/react-tablekit/api/utilities/): `createColumnHelper`, `defineColumns`. --- # Sorting > Click to sort, shift-click to add a column, and comparators that understand text, dates and your own domain order. URL: https://react-tablekit.vercel.app/react-tablekit/sorting/ ## Basics **Example: Sorting** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/sorting/demo-basics) ```tsx import { createColumnHelper, DataTable, type SortingFn } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const data = generatePeople(60); const col = createColumnHelper(); /** Seniority is an order, not an alphabet: "active" before "invited" before "suspended". */ const STATUS_ORDER: Record = { active: 0, invited: 1, suspended: 2, }; const byStatus: SortingFn = (rowA, rowB) => STATUS_ORDER[rowA.original.status] - STATUS_ORDER[rowB.original.status]; const columns = [ // `alphanumeric` is the natural comparator: "Room 2" sorts before "Room 10". col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name', sortingFn: 'alphanumeric', }), // Locale-aware, so "Ávila" lands next to "Avila" rather than after "Z". col.accessor('city', { header: 'City', sortingFn: 'text', // Rows with no city go last in both directions, instead of bubbling to the top. sortUndefined: 'last', }), col.accessor('department', { header: 'Department' }), // A custom comparator for a domain order no built-in can know. col.accessor('status', { header: 'Status', sortingFn: byStatus }), // Money and dates are almost always most-interesting-first on the first click. col.accessor('salary', { header: 'Salary', type: 'number', sortDescFirst: true }), col.accessor('joined', { header: 'Joined', type: 'date', sortDescFirst: true }), col.accessor('age', { header: 'Age', type: 'number' }), ]; /** * Client sorting. Click a header to sort, click again to reverse, and a third time to * clear it. Hold Shift and click to add a second and third column; the badge on each header * shows its priority, and the whole thing is one comparison chain. */ export default function ClientSortingExample() { return ( aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} enableSorting enableMultiSort initialState={{ sorting: [{ id: 'status', desc: false }], pagination: { pageIndex: 0, pageSize: 8 }, }} /> ); } ``` Sorting is on by default. A click cycles ascending → descending → unsorted; holding **Shift** adds a column to the sort instead of replacing it, and each header shows its position in the chain. ### Choosing a comparator The column `type` already picks a sensible comparator, so most tables need nothing here. Override it with `sortingFn` when the default ordering is wrong for your data: ```tsx col.accessor('name', { sortingFn: 'alphanumeric' }); // "Room 2" before "Room 10" col.accessor('city', { sortingFn: 'text' }); // locale-aware: "Ávila" next to "Avila" col.accessor('status', { sortingFn: (a, b) => RANK[a.original.status] - RANK[b.original.status], }); ``` The built-ins are `alphanumeric`, `alphanumericCaseSensitive`, `text`, `textCaseSensitive`, `datetime`, `basic` and `boolean`. ### Missing values and direction `sortUndefined` decides where empty cells go. `'last'` keeps them out of the way in both directions, which is usually what people expect. `sortDescFirst` makes the first click descending, which suits money and dates, and `invertSorting` suits ranks where lower is better. When the value you sort by differs from the value you display (a formatted date, a chip label), give the column a `sortValue`. ### Sorting on the server **Example: Server sorting** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/sorting/demo-server) ```tsx import { createColumnHelper, createLocalDataSource, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { RequestLog, useRequestLog } from '@/demo-support/requestLog'; const col = createColumnHelper(); /** The server sorts by its own field names, mapped with `sortServerKey`. */ const columns = [ col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name', sortServerKey: 'last_name', }), col.accessor('email', { header: 'Email', enableSorting: false }), col.accessor('department', { header: 'Department', sortServerKey: 'department_code' }), col.accessor('age', { header: 'Age', type: 'number', sortServerKey: 'age_years' }), col.accessor('joined', { header: 'Joined', type: 'date', sortServerKey: 'joined_at' }), ]; const people = generatePeople(120); const source = createLocalDataSource(people, { columns, getRowId: (p) => p.id, latencyMs: [150, 350], }); /** * Manual (server) sorting: the table emits `sorting` using each column's `sortServerKey`, and the * simulated server maps those keys back to its data. Shift+click adds a second sort column. */ export default function ServerSortingExample() { const { dataSource, log, clear } = useRequestLog(source); return (
p.id} manualSorting manualPagination enableMultiSort initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />
); } ``` In server mode the sort list is sent rather than applied. Map column ids to your API's field names with `sortServerKey`; nothing else changes. ```tsx col.accessor('name', { sortServerKey: 'LastName' }); ``` ## Customization The sort indicator, the clickable header and the header cell itself are all slots. Replacing the indicator alone is the common case: ```tsx }} /> ``` To change what a click _does_ rather than how it looks, wrap the `onSortToggle` handler; it can observe the intent, change it, or cancel it by not calling `next`. See [Handler middleware](/react-tablekit/customization/handlers/). ## Limitations - **Multi-sort priority cannot be reordered by dragging.** A column joins the chain in the order it was shift-clicked. To reorder, clear the sort and rebuild it, or set `state.sorting` yourself. - **Server mode sorts whatever the server returns.** If you enable client sorting alongside server pagination, only the fetched page is ordered, and the table warns about this in development unless you pass `acknowledgePageLocalSorting`. See [Client and server](/react-tablekit/client-and-server/). - **Grouped rows sort within their group**, never across groups; the group order comes from the grouping column. See [Grouping](/react-tablekit/grouping/). ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableSorting`, `enableMultiSort`, `enableSortingRemoval`, `sortingMode`, `onSortingChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `sortingFn`, `sortValue`, `sortDescFirst`, `sortUndefined`, `invertSorting`, `sortServerKey`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setSorting`, `resetSorting`. - [Utilities](/react-tablekit/api/utilities/): the `sortingFns` registry. --- # Global search > One input that matches across every searchable column, with debouncing, a minimum length and optional match highlighting. URL: https://react-tablekit.vercel.app/react-tablekit/global-search/ ## Basics **Example: Global search** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/global-search/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const people = generatePeople(60); const others = generatePeople(40, 7); /** * Client-side global search: debounce, minimum length, match highlighting and * diacritic-insensitive matching. Two tables share the `mod+k` hotkey to show that it is scoped * to the most recently focused or hovered table, never both at once. */ export default function GlobalSearchExample() { return (

Employees

Type zoe to match “Zoë”, or garcia to match “García”: matching folds case and diacritics. Matches are highlighted, the query is debounced by 200ms and ignored below 2 characters. Press Ctrl+K (⌘+K) while the pointer or focus is inside this table.

p.id} highlightSearchMatches searchMinLength={2} searchDebounceMs={200} searchHotkey="mod+k" initialState={{ pagination: { pageIndex: 0, pageSize: 5 } }} />

Contractors

The same hotkey, a different table. Whichever table you last touched wins, so the two never both react.

p.id} highlightSearchMatches searchMinLength={2} searchDebounceMs={200} searchHotkey="mod+k" globalFilterMatch="any-word" initialState={{ pagination: { pageIndex: 0, pageSize: 5 } }} />
); } ``` The toolbar search box matches every column that has not opted out. Matching ignores case and diacritics, so `zurich` finds `Zürich`. Escape clears the box while it has focus. Two props keep it from firing on every keystroke: `searchDebounceMs` waits for typing to stop, and `searchMinLength` ignores queries shorter than a threshold. Together they are the difference between one request and eight. ### What a word matches By default a multi-word query is an AND across words: `ada london` matches a row containing both, in any column and any order. `globalFilterMatch` changes that to `'any-word'`, or to `'phrase'` for a literal substring. A column decides what text it contributes. `getSearchValue` wins, then `format`, then the raw value stringified, so a formatted date is searched as it is displayed. Setting `enableGlobalFilter: false` removes a column from search entirely, which is worth doing for ids nobody types. ### Highlighting and the hotkey `highlightSearchMatches` wraps hits in a `mark` element inside text cells. `searchHotkey` (for example `'mod+k'`) focuses this table's box, and `showSearchHotkeyHint` prints the platform-correct chip next to it. With two tables on a page the hotkey goes to the one most recently focused or hovered, and it is ignored while you are typing in an unrelated field. ### Searching on the server In server mode the query travels as `search` in the `TableQuery` and the server decides what it means. Nothing about the control changes. See [Client and server](/react-tablekit/client-and-server/). ## Customization The box is a slot, and it is also a composable part: `` renders it anywhere on the page (a page header, a dialog title bar) while the table keeps owning the state. ```tsx ``` `searchPlaceholder` changes the prompt; every other string comes from [localization](/react-tablekit/localization/). ## Limitations - **Search filters, it does not rank.** Rows are kept or dropped, never scored or reordered. For fuzzy ranking use a `fuzzy` [column filter](/react-tablekit/column-filters/), or search on the server. - **Highlighting only applies to the default cell renderer.** A custom `cell` renders whatever you return, so the table cannot mark inside it. - **`searchMinLength` also gates the server request**, which means a two-character query shows the unfiltered data rather than an empty table. That is deliberate, and it is why the threshold defaults low. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableGlobalFilter`, `searchDebounceMs`, `searchMinLength`, `searchMode`, `searchPlaceholder`, `searchHotkey`, `showSearchHotkeyHint`, `globalFilterFn`, `globalFilterMatch`, `highlightSearchMatches`, `onGlobalFilterChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `enableGlobalFilter`, `getSearchValue`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setGlobalFilter`, `resetGlobalFilter`. --- # Column filters > Per-column filters in a panel, a popover or a filter row, with a control chosen from the column's type and values drawn from the data. URL: https://react-tablekit.vercel.app/react-tablekit/column-filters/ ## Basics **Example: Filter panel** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-filters/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(150); /** * The default filter panel: a "Filters" button with an active count, one control per * filterable column (text, select, multi-select, range, date range, boolean), active filter chips * with per-chip removal, and facet counts taken from the data. */ export default function FiltersPanelExample() { return ( p.id} filterDisplayMode="panel" showActiveFilterChips showFacetCounts initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` Each filterable column gets a control, and the control comes from the column: a `number` column filters by range, a `date` column by date, a column with `filterOptions` by selection. You can override it with `filterVariant`. The **Filters** button in the toolbar carries a badge with the number of active filters and toggles the panel. Active filters also appear as removable chips under the toolbar when `showActiveFilterChips` is on, which is what makes "why is this table empty?" answerable at a glance. ### Where the controls live **Example: Filter row** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-filters/demo-row-mode) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(150); /** * `filterDisplayMode="row"`: a second header row with an inline control under each * filterable column. Tab order follows the columns, so keyboard users can filter without leaving * the header. */ export default function FiltersRowExample() { return ( p.id} filterDisplayMode="row" showActiveFilterChips initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` `filterDisplayMode` decides the surface, not the behaviour: | Mode | What the reader sees | | ----------- | -------------------------------------------------------------- | | `'panel'` | A collapsible grid of controls under the toolbar. The default. | | `'popover'` | The same grid, anchored to the Filters button. | | `'row'` | A second header row with one input per column. | | `'none'` | No built-in UI; drive `state.columnFilters` yourself. | **Example: Filter popover and column menu** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-filters/demo-popover-mode) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(150); /** * `filterDisplayMode="popover"` plus `enableColumnActions`: the same controls in a * popover anchored to the Filters button, and a "Filter…" entry in each column's actions menu that * opens just that column's filter. */ export default function FiltersPopoverExample() { return ( p.id} filterDisplayMode="popover" enableColumnActions enableHiding showActiveFilterChips initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` Independently of the mode, `enableColumnActions` puts a **Filter…** entry in each header menu that opens just that column's filter. ### Applying instantly or on demand `filterApplyMode: 'manual'` adds an Apply button, so a reader can set three filters before the table moves. Combined with `filterDebounceMs` on text inputs, it is how a server-mode table avoids a request per keystroke. ### Choosing the comparison `filterFn` picks the comparison and `filterOperators` decides which ones the reader may choose from the operator dropdown. The built-ins cover text (`includesString`, `startsWith`, `endsWith`, `equalsString`, `fuzzy`), numbers (`equals`, `inNumberRange`), dates (`dateEquals`, `before`, `after`, `inDateRange`), arrays (`arrIncludes`, `arrIncludesAll`, `arrIncludesSome`) and emptiness (`empty`, `notEmpty`). ```tsx col.accessor('city', { filterVariant: 'multiSelect', filterFn: 'arrIncludesSome' }); col.accessor('price', { type: 'number', filterVariant: 'rangeSlider' }); ``` ### Filtering on the server Filters serialize into the `filters` array of the `TableQuery`, using `filterServerKey` when your API names a field differently. Option lists come from `dataSource.fetchFacets` instead of the loaded rows; see [Faceting](/react-tablekit/faceting/). ## Customization `renderFilter` replaces one column's control while the table keeps owning the value: ```tsx col.accessor('assignee', { filterVariant: 'custom', renderFilter: ({ value, setValue }) => , }); ``` The panel, the chips row and the Filters button are all [slots](/react-tablekit/customization/slots/), and `` places the panel anywhere on the page. ## Limitations - **A column has one active filter at a time.** Two conditions on the same column ("after March and before June") belong in a `range` or `dateRange` variant, or in a custom `filterFn`. - **Filters combine with AND.** There is no OR builder across columns; express that on the server, or with a single custom filter over a computed column. - **`'row'` mode has no operator dropdown.** It is one input per column by design; use `'panel'` or the column menu when operators matter. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableColumnFilters`, `enableFilters`, `filterDisplayMode`, `filterApplyMode`, `filterDebounceMs`, `filterMode`, `filterFns`, `showActiveFilterChips`, `datePresets`, `onColumnFiltersChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `enableColumnFilter`, `filterVariant`, `filterFn`, `filterOperators`, `filterOptions`, `filterPlaceholder`, `filterServerKey`, `renderFilter`, `multiSelectDisplay`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setColumnFilters`, `resetColumnFilters`, `clearAllFilters`, `getActiveFilterCount`. --- # Faceting > Filter options and counts computed from the data: from the loaded rows in client mode, from your API in server mode. URL: https://react-tablekit.vercel.app/react-tablekit/faceting/ ## Basics **Example: Server-side facets** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/faceting/demo-basics) ```tsx import { createLocalDataSource, DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; import { RequestLog, useRequestLog } from '@/demo-support/requestLog'; const people = generatePeople(400); const source = createLocalDataSource(people, { columns: peopleColumns, getRowId: (p) => p.id, latencyMs: [200, 450], }); /** * Server filtering and faceting: filters and the search are serialized into the query, * and the option lists plus their counts are loaded lazily through `dataSource.fetchFacets`, so * the browser never sees the full dataset. */ export default function FiltersServerExample() { const { dataSource, log, clear } = useRequestLog(source); return (
p.id} dataMode="server" showActiveFilterChips showFacetCounts searchDebounceMs={300} searchMinLength={2} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />
); } ``` A facet is the answer to "what can I filter this column by, and how many rows would each choice leave?" When a column has no `filterOptions`, the table works them out: the distinct values for a select or multi-select, and the minimum and maximum for a range or slider. Counts are shown next to each option, as in `Berlin (142)`, and they respect the _other_ active filters, so the numbers describe what you would actually get. ### Client and server In client mode the facets come from `column.getFacetedUniqueValues()` and `column.getFacetedMinMaxValues()` over the currently loaded rows, which is exact because the browser has all of them. In server mode the browser has one page, so counting locally would be wrong. The table asks the data source instead: ```tsx const dataSource: DataSource = { ...createRestDataSource({ url: '/api/people', mapQuery, mapResult }), fetchFacets: (columnId, query, { signal }) => api.facets(columnId, query, signal), }; ``` `fetchFacets` is a method of the data source rather than an option of `createRestDataSource`, so it is added beside the REST source's own `fetch`. It is asked for one column at a time. Facets are requested lazily, the first time a filter control opens, and cached per query, so opening the same dropdown twice costs one round trip. `manualFaceting` and `facetingMode` force the choice when the default inference is not what you want, and `facets` supplies them outright. ### Turning it off `enableFacetedValues: false` stops the computation for tables where the option lists are static and known. Then `filterOptions` on the column is the only source, and no counts are shown. `showFacetCounts: false` keeps the options but drops the numbers. ## Customization `filterOptions` accepts a static array, a function, or an async function; the control shows a loading state while it resolves. Options carry their own presentation, which is what makes a status filter look like the status column: ```tsx col.accessor('status', { filterVariant: 'select', filterOptions: [ { value: 'active', label: 'Active', color: '#e6f4ea' }, { value: 'archived', label: 'Archived', color: '#f1f3f4' }, ], }); ``` Those colours are reused by the [filter chips](/react-tablekit/column-filters/). ## Limitations - **Client facets only see loaded rows.** With server pagination the browser holds one page, so faceting must come from the server or the counts will describe the page rather than the data set. - **Counts reflect the other filters, not this one.** A multi-select shows what each option would add, which is the standard behaviour but is not the same as the count after applying it. - **There is no facet for a display column.** A facet needs a value; compute it with an `accessorFn` if you need to filter on something derived. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableFacetedValues`, `facetingMode`, `manualFaceting`, `facets`, `showFacetCounts`. - [`ColumnDef`](/react-tablekit/api/column-def/): `filterOptions`, `filterVariant`. - [`TableInstance`](/react-tablekit/api/table-instance/): `getColumn(id).getFacetedUniqueValues`, `getFacetedMinMaxValues`, `getServerFacets`, `loadFacets`. - [Utilities](/react-tablekit/api/utilities/): `createRestDataSource`. --- # Pagination > Six presentations of the same state (numbered, compact, simple, load-more, infinite or none), including cursor-based paging. URL: https://react-tablekit.vercel.app/react-tablekit/pagination/ ## Basics **Example: Pagination variants** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/pagination/demo-basics) ```tsx import { useId, useState } from 'react'; import { DataTable, getPageItems, type PageItem, type PaginationVariant, } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(235); const columns = peopleColumns.slice(0, 4); const VARIANTS: PaginationVariant[] = ['numbered', 'compact', 'simple', 'loadMore']; const render = (items: PageItem[]) => items .map((i) => (i.type === 'page' ? (i.selected ? `[${i.index + 1}]` : String(i.index + 1)) : '…')) .join(' '); /** Algorithm visualizer: classic (boundary + window rule) vs stable (constant width). */ function Visualizer() { const [pageCount, setPageCount] = useState(24); const [pageIndex, setPageIndex] = useState(0); const pageId = useId(); const countId = useId(); const classic = getPageItems({ pageIndex, pageCount }); const stable = getPageItems({ pageIndex, pageCount, algorithm: 'stable' }); return (

Page-item algorithms

classic ({classic.length} items)
{render(classic)}
stable ({stable.length} items)
{render(stable)}
); } /** Every pagination variant side by side, plus the page-size selector and row range. */ export default function PaginationVariantsExample() { return (
{VARIANTS.map((variant) => (

{variant}

p.id} toolbar={false} initialState={{ pagination: { pageIndex: 0, pageSize: 5 } }} pagination={{ variant, pageSizeOptions: [5, 10, 25], showRowRange: true, showFirstLast: variant === 'compact', }} />
))}
); } ``` Pagination is one piece of state (page index and page size) and several ways to show it. `paginationType` picks the presentation: | Variant | Shape | | ---------- | -------------------------------------------------------------------------- | | `numbered` | Previous, page buttons, Next. The desktop default. | | `compact` | First/previous/next/last icons with a tight page list. The mobile default. | | `simple` | `11–20 of 235` between two arrows. Required for cursor paging. | | `loadMore` | A button that appends the next page to the rows already shown. | | `infinite` | The next page loads when the end of the list comes into view. | | `none` | No control; the state still applies. | The variant accepts a responsive value, so the classic default is literally `{ base: 'compact', md: 'numbered' }`: small screens get icons, wide screens get page numbers. ### Rows per page `pageSizeOptions` fills the "Rows per page" select. Changing the size keeps the first visible row on screen rather than throwing the reader back to page 1. `showRowRange` prints the range and total, and when the total is unknown (a server that does not count) it says `11–20 of many` instead of inventing a number. ### Which page buttons appear `getPageItems` is the pure function behind the number row, and you can call it yourself. The default `classic` algorithm never hides exactly one page behind an ellipsis: if the gap is a single number, it shows the number. The `stable` algorithm always renders the same number of slots, so the bar never changes width as you page through. ### Cursor paging **Example: Cursor pagination** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/pagination/demo-cursor) ```tsx import { createLocalDataSource, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; import { RequestLog, useRequestLog } from '@/demo-support/requestLog'; const people = generatePeople(500); // The local data source answers with `nextCursor` / `prevCursor`, like a cursor API. const source = createLocalDataSource(people, { columns: peopleColumns, getRowId: (p) => p.id, latencyMs: [150, 350], }); /** * Cursor pagination: the server returns opaque cursors instead of a page count, so the * numbered variant is unavailable and the table shows “of many”. Next and Previous follow the * cursors the server sent, and the request log shows them. */ export default function CursorPaginationExample() { const { dataSource, log, clear } = useRequestLog(source); return (
aria-label="People" columns={peopleColumns} dataSource={dataSource} getRowId={(p) => p.id} paginationType="cursor" pagination={{ variant: 'simple', showRowRange: true }} initialState={{ pagination: { pageIndex: 0, pageSize: 10, cursor: null } }} />
); } ``` When the API hands back a cursor rather than a page number, the table follows it: Next is enabled while the server says there is more, and Previous walks back through the cursors already seen. Page _numbers_ are not available in this mode, which is why `simple` is the required variant. ### Infinite scrolling **Example: Infinite scroll** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/pagination/demo-infinite) ```tsx import { createLocalDataSource, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const people = generatePeople(5_000); const source = createLocalDataSource(people, { columns: peopleColumns, getRowId: (p) => p.id, latencyMs: [200, 400], }); /** * Infinite pagination: instead of page buttons, reaching the end of the scroll area * loads the next page and appends it, with virtualization keeping the DOM small. A search or a * sort starts the list again from the first page. */ export default function InfiniteScrollExample() { return ( aria-label="People" columns={peopleColumns} dataSource={source} getRowId={(p) => p.id} pagination={{ variant: 'infinite' }} enableRowVirtualization enableStickyHeader maxHeight={460} estimateRowHeight={44} initialState={{ pagination: { pageIndex: 0, pageSize: 50 } }} /> ); } ``` `infinite` observes a sentinel at the end of the rows and fetches the next page when it appears. It composes with [virtualization](/react-tablekit/virtualization/), so a hundred thousand accumulated rows still render a screenful at a time. ## Customization The whole control is a slot, and `` places it anywhere: above the table, in a sticky page footer, beside a summary. `formatters.rowRange` rewrites the range text, and every label comes from [localization](/react-tablekit/localization/). `hideOnSinglePage` is on by default: with one page there is nothing to navigate, so the control disappears and the container takes its bottom border back. ## Limitations - **Cursor mode has no page count**, so it cannot show page numbers or a "last page" button, and `showRowRange` needs a total from the server to print one. - **`loadMore` and `infinite` accumulate rows.** Sorting or filtering resets the accumulation to the first page, because the rows already loaded are no longer the first rows of the result. - **Page state is not automatically preserved across mounts.** Put `pagination` in [`syncState`](/react-tablekit/persistence/) if a reader should come back to the page they left. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enablePagination`, `paginationType`, `paginationMode`, `pagination`, `pageCount`, `rowCount`, `manualPagination`, `appendPages`, `resetPageOn`, `autoResetPageIndex`, `scrollToTopOnPageChange`, `disablePaginationWhileFetching`, `compactPagination`, `onPaginationChange`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setPageIndex`, `setPageSize`, `nextPage`, `previousPage`, `firstPage`, `lastPage`, `getPageCount`, `getPageItems`, `getCursors`. - [Utilities](/react-tablekit/api/utilities/): `getPageItems`. --- # Row selection > Checkboxes or radios, range selection with shift-click, a selection bar for bulk actions, and select-all that survives server paging. URL: https://react-tablekit.vercel.app/react-tablekit/row-selection/ ## Basics **Example: Row selection** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/row-selection/demo-basics) ```tsx import { useState } from 'react'; import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(60); /** * Row selection: multi with a tri-state header checkbox, Shift+click range selection, * row-click selection, disabled rows that select-all skips, and a selection bar with bulk actions. * Switch to single mode to get radio semantics, as a picker dialog would. */ export default function RowSelectionExample() { const [multi, setMulti] = useState(true); const [log, setLog] = useState(null); return (

Suspended people are disabled: they cannot be selected, and “select all” skips them.

key={multi ? 'multi' : 'single'} aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enableRowSelection={(row) => row.original.status !== 'suspended'} enableMultiRowSelection={multi} enableRangeSelection={multi} selectOnRowClick renderBulkActions={({ selectedRows }) => ( )} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />

{log ?? ''}

); } ``` `enableRowSelection` inserts a selection column and a selection bar. The header checkbox is tri-state (all, some, none), and shift-clicking a second row selects the range between them. Set `enableMultiRowSelection: false` for picker semantics: radios instead of checkboxes, no header checkbox, one row at a time. `selectOnRowClick` makes the whole row the target, and it still ignores clicks that land on a button, a link, an input, or anything marked `data-tk-stop`, so the row menu keeps working. ### Which rows may be selected `enableRowSelection` also accepts a predicate, and `isRowDisabled` marks rows as unavailable everywhere at once. Select-all skips disabled rows rather than silently including them. ```tsx row.original.status !== 'archived'} /> ``` With [tree data](/react-tablekit/tree-data/), `enableSubRowSelection` cascades a parent's checkbox to its children and shows the parent as indeterminate when only some are selected. ### The selection bar and bulk actions The bar appears once something is selected: `3 selected · Select all 235 matching · Clear`, plus whatever `renderBulkActions` returns. It is the place for Delete, Export and Assign, and it gets the selection in a form you can send: ```tsx renderBulkActions={({ selectedRows, selectionQuery }) => ( )} ``` ### Selecting everything on the server "Select all 235 matching" cannot mean 235 ids when the browser holds ten rows. It is recorded as the current query plus the rows the reader has since _deselected_, and `getSelectionQuery()` hands that to you, so a bulk action applies to the filter, not to a page. `selectAllMode` chooses between the page and the whole result set. ## Customization The checkbox, the selection column and the selection bar are [slots](/react-tablekit/customization/slots/). `positionSelectionColumn` moves the column, and `` renders the bar outside the table, at the bottom of a dialog, for instance. Selected rows carry `aria-selected` and `data-selected`, and their background is the `--tk-row-selected-bg` [token](/react-tablekit/customization/tokens/); pinned cells inherit it, so a selected row looks selected all the way across. ## Limitations - **Selection is keyed by row id.** Without a stable `getRowId` the selection resets whenever the data is refetched, because the rows are new objects. - **`selectionQuery` is an exclusion model, not a list of ids.** Your API has to accept a query with exclusions; if it only accepts ids, keep `selectAllMode` on the page. - **Cross-page selection is not remembered in client mode.** Paging keeps the selection because the rows stay loaded; refetching does not. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableRowSelection`, `enableMultiRowSelection`, `enableSubRowSelection`, `enableRangeSelection`, `selectOnRowClick`, `selectAllMode`, `showSelectionBar`, `renderBulkActions`, `getRowCanSelect`, `isRowDisabled`, `positionSelectionColumn`, `clearSelectionOnQueryChange`, `autoResetSelection`, `onRowSelectionChange`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setRowSelection`, `resetRowSelection`, `toggleAllRowsSelected`, `toggleAllPageRowsSelected`, `selectAllMatching`, `selectRange`, `getSelectedRowModel`, `getSelectedRowIds`, `getSelectedCount`, `getSelectionQuery`. - [`Row`](/react-tablekit/api/table-instance/): `toggleSelected`, `getIsSelected`, `getCanSelect`. --- # Expansion > Detail panels that open under a row, lazily rendered, animated, and pinned to the viewport width so a wide table cannot scroll them away. URL: https://react-tablekit.vercel.app/react-tablekit/expansion/ ## Basics **Example: Detail panels** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/expansion/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(40); /** * Collapsible row content: `renderDetailPanel` adds a full-width row under the expanded * one, with an expand toggle column. The panel is sticky-left, so it stays put while a wide table * scrolls sideways. */ export default function DetailPanelsExample() { return ( aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} renderDetailPanel={({ row }) => (
Email
{row.original.email}
City
{row.original.city ?? 'Unknown'}
Joined
{row.original.joined}
Remote
{row.original.remote ? 'Yes' : 'No'}
)} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` `renderDetailPanel` puts a full-width row under the row it belongs to, and an expand column with a chevron appears automatically. ```tsx } columns={columns} data={orders} /> ``` The panel is not rendered until it is opened, so a hundred collapsed rows cost nothing. Pass `detailPanelProps.keepMounted` when the content is expensive to rebuild and should survive a collapse. ### Loading panel content A panel usually needs data the table never fetched. `useDetailPanelData` loads it per row and caches it by row id, so re-opening is instant: ```tsx function Panel({ row }) { const { data, loading, error } = useDetailPanelData(row, () => api.lines(row.id)); if (loading) return ; if (error) return ; return ; } ``` ### One at a time `expandMode: 'single'` collapses the previous panel when another opens, like an accordion. `expandOnRowClick` makes the whole row the toggle, with the same exclusion for buttons and links that [row selection](/react-tablekit/row-selection/) uses. `enableExpandAll` puts an expand-all toggle in the header. ### Rows with children Sub-rows use the same expansion state and the same toggle. That is a capability of its own; see [Tree data](/react-tablekit/tree-data/). ## Customization The panel is sticky to the left at the container's width, so scrolling a wide table sideways does not drag the panel out of view. `detailPanelProps.fullWidth: false` opts out and lets the panel scroll with the columns. The toggle is a [slot](/react-tablekit/customization/slots/), and `positionExpandColumn` moves the column to the other end. The open/close animation is a CSS grid transition that `prefers-reduced-motion` disables. ## Limitations - **A row has one panel.** Two kinds of detail (history and comments) belong in tabs inside the panel, not in two panels. - **A panel is not a cell.** It is a separate row, so it takes no part in sorting, filtering, search or CSV export. - **The expanded state is keyed by row id**, so it resets on refetch unless `getRowId` is stable and `autoResetExpanded` is off. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableExpanding`, `renderDetailPanel`, `detailPanelProps`, `expandMode`, `expandOnRowClick`, `enableExpandAll`, `getRowCanExpand`, `positionExpandColumn`, `paginateExpandedRows`, `autoResetExpanded`, `manualExpanding`, `onExpandedChange`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setExpanded`, `resetExpanded`, `toggleAllRowsExpanded`, `getIsAllRowsExpanded`, `getExpandedRowModel`. - [`Row`](/react-tablekit/api/table-instance/): `toggleExpanded`, `getIsExpanded`, `getCanExpand`. - [Hooks](/react-tablekit/api/hooks/): `useDetailPanelData`. --- # Client and server > The same table and the same props, with the work done in the browser, on your API, or split feature by feature. URL: https://react-tablekit.vercel.app/react-tablekit/client-and-server/ ## Basics **Example: Client and server** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/client-and-server/demo-basics) ```tsx import { useMemo, useState } from 'react'; import { createLocalDataSource, DataTable, useDataTable, type SortingState, type TableInstance, } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(500, 7); /** * The same dataset twice: the left table computes everything in the browser; the right * one sends each query to `createLocalDataSource`, which simulates a server (latency, abort) with * the engine's own filter/sort functions. Identical interactions give identical rows. */ export default function ClientVsServerExample() { const [latency, setLatency] = useState(400); const server = useMemo( () => createLocalDataSource(data, { columns: peopleColumns, getRowId: (p) => p.id, latencyMs: [latency / 2, latency], }), [latency], ); const common = { columns: peopleColumns, getRowId: (p: DemoPerson) => p.id, toolbar: false as const, searchDebounceMs: 300, }; const client = useDataTable({ ...common, data, 'aria-label': 'Client mode' }); const remote = useDataTable({ ...common, dataSource: server, 'aria-label': 'Server mode', loadingDisplay: 'skeleton', }); const [search, setSearch] = useState(''); const [sort, setSort] = useState(''); const apply = (fn: (t: TableInstance) => void) => { fn(client); fn(remote); }; const sorting: SortingState = sort ? [{ id: sort.replace(/^-/, ''), desc: sort.startsWith('-') }] : []; const clientIds = client .getRowModel() .rows.map((r) => r.id) .join(); const serverIds = remote .getRowModel() .rows.map((r) => r.id) .join(); const status = remote.getDataStatus(); const identical = !status.fetching && !status.loading && clientIds === serverIds; return (
{identical ? 'Identical rows ✓' : 'Waiting for the server…'}

Client mode

Server mode (simulated)

); } ``` A table in client mode holds all the rows and computes everything itself. A table in server mode holds one page and asks the API. Both are the same component with the same props; what changes is who does the work. ```tsx p.id} /> p.id} /> ``` `dataMode="server"` switches pagination, sorting, filtering, search, grouping and faceting to the server at once. Expansion stays local, because sub-rows can still come from the data you already have. ### The query Whichever mode you are in, the table maintains one normalized query: page, sort list, search term, column filters and grouping. That object is what a server mode table sends, and you can read it at any time with `table.getQuery()`. `onQueryChange(query, change)` fires once, after debouncing, whenever something that affects server data changes, and it tells you _why_ it changed. It is the single place to hang a fetch, instead of six callbacks racing each other. ```tsx void refetch(query)} data={page.items} rowCount={page.total} /> ``` The per-slice callbacks (`onSortingChange`, `onPaginationChange` and the rest) still fire immediately, for controlled state. ### A data source instead of an effect `dataSource` packages the fetch, so the table drives the request itself and gets loading, error, refetch and facet handling for free: ```tsx interface PeoplePage { content: Person[]; totalElements: number; } const dataSource = createRestDataSource({ url: '/api/people', mapQuery: (query) => ({ page: query.pagination.pageIndex, size: query.pagination.pageSize, sort: query.sorting.map((s) => s.id + ',' + (s.desc ? 'desc' : 'asc')), q: query.globalFilter, }), mapResult: (body) => ({ rows: body.content, rowCount: body.totalElements }), }); ``` `mapQuery` turns the table's query into the request, and `mapResult` turns the response into rows and a total. The second type argument is the response's shape, which is what lets `mapResult` read it. `keepPreviousData` keeps the old page visible while the next one loads, `dedupeMs` collapses duplicate requests, and `refetchInterval` and `refetchOnWindowFocus` cover live data. ### Mixing the two **Example: Hybrid mode** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/client-and-server/demo-hybrid) ```tsx import { createLocalDataSource, DataTable } from '@kingtech/react-tablekit'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; import { RequestLog, useRequestLog } from '@/demo-support/requestLog'; const people = generatePeople(300); const source = createLocalDataSource(people, { columns: peopleColumns, getRowId: (p) => p.id, latencyMs: [150, 300], }); /** * Hybrid mode: the server paginates, the browser sorts the page it was given. That sorts * only the current page, which is why the engine warns unless you acknowledge it with * `acknowledgePageLocalSorting`. Sorting here never triggers a request; watch the log. */ export default function HybridModeExample() { const { dataSource, log, clear } = useRequestLog(source); return (
p.id} sortingMode="client" acknowledgePageLocalSorting initialState={{ pagination: { pageIndex: 0, pageSize: 10 } }} />
); } ``` Per-feature modes override `dataMode`, so you can page and filter on the server while sorting the loaded page in the browser: ```tsx ``` That combination sorts _the current page only_, which is sometimes exactly what you want and sometimes a bug. The table warns about it in development until you acknowledge it in the props, which is also the documentation of the decision for whoever reads the code next. ## Customization `createLocalDataSource` gives an in-memory array the same interface as a REST source, which makes tests and prototypes behave like production. `useDataSource` exposes the same machinery outside a table, and [`useDataTable`](/react-tablekit/headless/) gives you the whole engine with no markup. ## Limitations - **Server grouping with client pagination is not supported** and throws in development: the table cannot slice group rows it did not build. - **Client faceting under server pagination describes the loaded page**, not the result set. Use `fetchFacets`; see [Faceting](/react-tablekit/faceting/). - **A server that does not return a total** cannot produce page numbers. Pass `rowCount: -1` and the table shows `of many` and switches to cursor-style navigation. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `dataMode`, `dataSource`, `paginationMode`, `sortingMode`, `filterMode`, `searchMode`, `groupingMode`, `facetingMode`, the `manual*` flags, `onQueryChange`, `rowCount`, `pageCount`, `keepPreviousData`, `dedupeMs`, `fetchOnMount`, `refetchInterval`, `refetchOnWindowFocus`, `acknowledgePageLocalSorting`, `acknowledgePageLocalFiltering`. - [`TableState`](/react-tablekit/api/table-state/): `TableQuery` and every state slice. - [`TableInstance`](/react-tablekit/api/table-instance/): `getQuery`, `flushQuery`, `refresh`, `invalidate`, `getDataStatus`. - [Utilities](/react-tablekit/api/utilities/): `createRestDataSource`, `createLocalDataSource`. --- # Tree data > Hierarchical rows with indentation, expand toggles and children that can be loaded from the server when a branch is opened. URL: https://react-tablekit.vercel.app/react-tablekit/tree-data/ ## Basics **Example: Tree data** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/tree-data/demo-basics) ```tsx import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { buildOrgTree, type OrgNode } from '@/demo-support/orgTree'; const col = createColumnHelper(); const columns = [ col.accessor('name', { header: 'Name' }), col.accessor('role', { header: 'Role' }), col.accessor('headcount', { header: 'Headcount', type: 'number' }), col.accessor('location', { header: 'Location' }), ]; const data = buildOrgTree(); /** * Tree data: `getSubRows` turns nested records into expandable rows with indentation, * an expand toggle in the first column, and “expand all” in the toolbar. Filtering keeps parents * whose descendants match. */ export default function TreeDataExample() { return ( aria-label="Organisation" data={data} columns={columns} getRowId={(n) => n.id} getSubRows={(n) => n.children} enableExpanding initialState={{ pagination: { pageIndex: 0, pageSize: 12 } }} /> ); } ``` `getSubRows` turns a flat list of objects into a tree. Every row keeps its depth, the first visible data column is indented by it, and the expand toggle appears on rows that have children. ```tsx row.children} columns={columns} /> ``` Children are ordinary rows: they sort, filter, select and export like any other. Sorting orders siblings within their parent rather than flattening the tree. ### Loading children when a branch opens **Example: Lazy server children** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/tree-data/demo-lazy-children) ```tsx import { createColumnHelper, DataTable, type DataSource } from '@kingtech/react-tablekit'; import { childrenOf, hasChildren, type OrgNode } from '@/demo-support/orgTree'; const col = createColumnHelper(); const columns = [ col.accessor('name', { header: 'Name' }), col.accessor('role', { header: 'Role' }), col.accessor('headcount', { header: 'Headcount', type: 'number' }), col.accessor('location', { header: 'Location' }), ]; const delay = (ms: number, signal: AbortSignal) => new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }); }); /** Roots come from `fetch`; each expansion asks the server for that node's children. */ const orgDataSource: DataSource = { async fetch(query, { signal }) { await delay(250, signal); const roots = childrenOf(null); const { pageIndex, pageSize } = query.pagination; return { rows: roots.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize), rowCount: roots.length, }; }, async fetchChildren(row, _query, { signal }) { await delay(400, signal); return childrenOf(row.id); }, }; /** * Lazily loaded tree rows: only the roots are fetched up front, and expanding a row * calls `dataSource.fetchChildren` once per row per query, showing a spinner in that row while it * loads. `getRowCanExpand` tells the table which rows are worth a toggle. */ export default function TreeLazyServerExample() { return ( aria-label="Organisation (lazy)" columns={columns} dataSource={orgDataSource} getRowId={(n) => n.id} getRowCanExpand={(row) => hasChildren(row.id)} enableExpanding toolbar={false} initialState={{ pagination: { pageIndex: 0, pageSize: 10 } }} /> ); } ``` A tree that is too big to send at once loads a branch at a time. `dataSource.fetchChildren` is called when a row is expanded; the toggle shows a spinner while it runs and reports an error inline if it fails. The table cannot know whether an unloaded row _has_ children, so tell it: ```tsx row.original.childCount > 0} /> ``` Loaded children are cached per row and invalidated when the query changes, so re-opening a branch does not refetch and a new filter does not show stale children. ### Filtering a tree By default a row is matched on its own values. `filterFromLeafRows` keeps a parent whenever any descendant matches and auto-expands the path to it, the behaviour you want when searching a file tree. `maxLeafRowFilterDepth` caps how deep that search goes. ### Selection `enableSubRowSelection` cascades a parent's checkbox down to its children, and shows the parent as indeterminate when only some are selected. See [Row selection](/react-tablekit/row-selection/). ## Customization `treeColumnId` moves the indentation and the toggle to a specific column instead of the first visible one. The indentation step is the `--tk-tree-indent` token, and the toggle is a [slot](/react-tablekit/customization/slots/), so a file tree can show folder icons rather than chevrons. A selectable tree is announced as a `treegrid`, so assistive technology gets the level and expanded state without any extra attributes from you. ## Limitations - **Sorting never reorders across levels.** Children stay inside their parent; that is what makes it a tree rather than a list. - **Pagination counts top-level rows** unless `paginateExpandedRows` is set, so an expanded branch does not push its siblings onto the next page. - **Lazily loaded children cannot be searched before they are loaded.** Client-side search only sees rows the browser has; move search to the server for a lazy tree. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `getSubRows`, `getRowCanExpand`, `treeColumnId`, `filterFromLeafRows`, `maxLeafRowFilterDepth`, `paginateExpandedRows`, `enableSubRowSelection`, `enableExpandAll`, `manualExpanding`. - [`Row`](/react-tablekit/api/table-instance/): `depth`, `subRows`, `getParentRow`, `getLeafRows`, `getCanExpand`, `toggleExpanded`. - [`TableInstance`](/react-tablekit/api/table-instance/): `getRowChildrenStatus`, `getExpandedRowModel`. --- # Grouping > Group rows by one column or several, with aggregates in the group row and totals in the footer. URL: https://react-tablekit.vercel.app/react-tablekit/grouping/ ## Basics **Example: Grouping and aggregation** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/grouping/demo-basics) ```tsx import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const col = createColumnHelper(); const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }); const columns = [ col.accessor('department', { header: 'Department', enableGrouping: true }), col.accessor('city', { header: 'City', enableGrouping: true }), col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name' }), col.accessor('age', { header: 'Age', type: 'number', aggregationFn: 'mean', footer: 'Average age', }), col.accessor('salary', { header: 'Salary', type: 'number', format: (v) => money.format(v), aggregationFn: 'sum', footer: 'Payroll', }), ]; const data = generatePeople(200); /** * Grouping and aggregation: group by one or more columns from the column menu, and the * group rows show the value, the row count and the aggregates of the other columns. The footer * aggregates the whole filtered set, not just the page. */ export default function GroupingAggregationExample() { return ( aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} enableGrouping enableColumnActions enableColumnFooters groupedColumnMode="reorder" initialState={{ grouping: ['department'], pagination: { pageIndex: 0, pageSize: 25 }, }} /> ); } ``` `enableGrouping` lets a column collapse its rows into groups. A group row shows the value, the number of rows under it and a toggle; the other columns show whatever their `aggregationFn` computes: a sum for an amount, an average for a rating, a count for anything. ```tsx col.accessor('country', { enableGrouping: true }); col.accessor('total', { type: 'currency', aggregationFn: 'sum' }); ``` `state.grouping` is an array, so grouping by country and then by city is a two-element list and the group rows nest. ### Where a grouped column goes `groupedColumnMode` decides what happens to the column you grouped by: `'reorder'` moves it to the front, `'remove'` hides it because the group row already says it, and `false` leaves it where it is. A reader can group from the column menu, and, when [column ordering](/react-tablekit/column-ordering/) is on, by dragging a header into the toolbar drop zone. ### Aggregating a custom value `aggregationFn` accepts a name or a function, and `aggregatedCell` renders the result. That is the difference between a number and `12 of 40 overdue`: ```tsx col.accessor('status', { aggregationFn: (ids, rows) => rows.filter((r) => r.original.overdue).length, aggregatedCell: ({ getValue, row }) => getValue() + ' of ' + row.subRows.length + ' overdue', }); ``` ### Footers `enableColumnFooters` renders a `tfoot` with the same aggregates across the whole table. `footerAggregationScope` chooses between the current page and everything the filters left; see [Column footers](/react-tablekit/column-footers/). ### Grouping on the server With `groupingMode: 'server'` the server returns group rows and the table renders them as they arrive. It does not re-group anything locally. ## Customization `groupedCell` replaces the group row's leading cell, `getGroupingValue` changes what a row groups _by_ without changing what it displays (bucketing dates by month, for instance), and the group toggle is a [slot](/react-tablekit/customization/slots/). ## Limitations - **Server grouping requires server pagination.** Grouping on the server and paging in the browser throws in development, because the table cannot slice groups it did not build. - **Aggregates ignore hidden rows, not hidden columns.** A hidden column still aggregates; a filtered-out row does not count. - **Grouping by more than two or three columns** is legal and rarely readable. A pivot is a different component. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableGrouping`, `groupingMode`, `manualGrouping`, `groupedColumnMode`, `aggregationFns`, `enableColumnFooters`, `footerAggregationScope`, `onGroupingChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `enableGrouping`, `aggregationFn`, `aggregatedCell`, `groupedCell`, `getGroupingValue`, `footer`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setGrouping`, `resetGrouping`, `getGroupedRowModel`, `getFooterGroups`. - [`Column`](/react-tablekit/api/table-instance/): `toggleGrouping`, `getIsGrouped`, `getAggregationFn`. --- # Column pinning > Freeze columns to either edge, stacked and responsive, with a shadow that appears only when content actually scrolls underneath. URL: https://react-tablekit.vercel.app/react-tablekit/column-pinning/ ## Basics **Example: Column pinning** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-pinning/demo-basics) ```tsx import { useState } from 'react'; import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const data = generatePeople(60); const col = createColumnHelper(); const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }); /** * `pin` on a column is the declarative form; `lockPin` stops the user changing it from the * column menu. A pin may also be responsive: here the name stays pinned only from `md` up, * because on a phone a pinned column would eat most of the screen. */ const columns = [ col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name', pin: { base: false, md: 'left' }, lockPin: true, }), col.accessor('email', { header: 'Email' }), col.accessor('department', { header: 'Department' }), col.accessor('city', { header: 'City' }), col.accessor('age', { header: 'Age', type: 'number' }), col.accessor('joined', { header: 'Joined', type: 'date' }), col.accessor('status', { header: 'Status' }), col.accessor('remote', { header: 'Remote', type: 'boolean' }), col.accessor('salary', { header: 'Salary', type: 'number', format: (v) => money.format(v), pin: 'right', }), ]; /** * Pinned columns stay put while the rest scrolls sideways, and the scroll shadows * show which side still has content. Flip the direction to see that pinning follows the * writing direction: in RTL the "left" side is rendered on the right. */ export default function ColumnPinningExample() { const [dir, setDir] = useState<'ltr' | 'rtl'>('ltr'); return (
aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} dir={dir} enableColumnPinning enableColumnActions maxHeight={420} enableStickyHeader initialState={{ pagination: { pageIndex: 0, pageSize: 20 } }} />
); } ``` A pinned column stays put while the rest of the table scrolls sideways. Pin it on the column, or put it in state, or let the reader do it from the header menu: ```tsx col.accessor('name', { pin: 'left' }); col.display({ id: 'actions', pin: 'right', static: true }); ``` Any number of columns can be pinned on each side. Their offsets stack, so the second left-pinned column sits exactly after the first however wide it is. ### The edge shadow The shadow at the boundary is not decoration that is always there: it appears only when content is actually scrolled underneath, which is what makes it mean something. It is the `--tk-pinned-shadow` [token](/react-tablekit/customization/tokens/), and the classic preset sets it to `none`. ### Pinning only on wide screens `pin` takes a responsive value, so a column can be frozen where there is room and released where there is not: ```tsx col.display({ id: 'actions', pin: { base: false, md: 'right' } }); ``` `stickyActions` is the shorthand for the common case: an actions column pinned right on desktop. ### Columns a reader may not move `static: true` locks a column completely: it cannot be unpinned, hidden, reordered, resized or edited from the menu. It is what the selection, expand and actions columns use. `lockPin` locks only the pinning. ## Customization Pinned cells take their background from `--tk-row-bg`, which follows hover, striping and selection, so a selected row stays selected across the frozen edge. In right-to-left text the sides swap, because the layout uses logical properties rather than left and right. ## Limitations - **Pinning needs a horizontal scroll to be visible.** In a table that fits, a pinned column looks like any other. - **A pinned column is still in the tab order where it sits**, not where it appears. Pinning is presentation, not structure. - **Very wide pinned columns can leave no room to scroll.** The table does not enforce a maximum; that judgement is yours. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableColumnPinning`, `stickyActions`, `rowActionsColumn`, `positionActionsColumn`, `onColumnPinningChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `pin`, `lockPin`, `static`, `lockPosition`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setColumnPinning`, `resetColumnPinning`, `getEffectiveColumnPinning`, `getLeftHeaderGroups`, `getRightHeaderGroups`. - [`Column`](/react-tablekit/api/table-instance/): `pin`, `getIsPinned`, `getCanPin`. --- # Column sizing > Percentage or pixel widths, a keyboard-operable resize handle, and double-click autosizing that measures the rendered rows. URL: https://react-tablekit.vercel.app/react-tablekit/column-sizing/ ## Basics **Example: Column sizing** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-sizing/demo-basics) ```tsx import { useState } from 'react'; import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const data = generatePeople(40); const col = createColumnHelper(); /** * Two ways to size a column, mixed deliberately: * * - `width` is CSS, so a percentage stays proportional as the table grows; * - `size` is a pixel number and is what dragging the resize handle changes, * bounded by `minSize` / `maxSize`. */ const columns = [ col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name (30%)', width: '30%', minSize: 120, }), col.accessor('email', { header: 'Email (size 260)', size: 260, minSize: 140, maxSize: 420 }), col.accessor('department', { header: 'Department', size: 160 }), col.accessor('city', { header: 'City', size: 120 }), // A fixed, unresizable column: the handle is not rendered at all. col.accessor('age', { header: 'Age', type: 'number', size: 80, enableResizing: false }), ]; /** * Column sizing. Drag a header's trailing edge, or focus it and press ←/→ (Shift for * a 50px step). `'onChange'` resizes live while dragging; `'onEnd'` waits for the release, * which is the better choice for very wide tables. Double-click a handle (or use "Autosize" * in the ⋮ menu) to fit the column to its content. */ export default function ColumnSizingExample() { const [mode, setMode] = useState<'onChange' | 'onEnd'>('onChange'); return (
aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} enableColumnResizing columnResizeMode={mode} enableColumnActions initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />
); } ``` There are two width models, and which one is in play depends on whether the reader can resize. Without resizing, widths are CSS: `width`, `minWidth` and `maxWidth` accept percentages or pixels, and they are applied through a `colgroup` so the header and the body always agree. ```tsx col.accessor('name', { width: '30%', minWidth: 160 }); ``` With `enableColumnResizing`, the table switches to pixels (`size`, `minSize`, `maxSize`) and converts any percentage on first measurement. A dragged column needs a number to drag from. ### The resize handle The handle on the header's trailing edge is a separator with a value, which means the keyboard works: arrow keys move it 10 pixels and Shift moves it 50. Double-clicking autosizes the column to the content of the rendered rows. `columnResizeMode: 'onChange'` resizes live as you drag; `'onEnd'` shows a guide line and applies the width when you let go, which is the calmer choice for a table with expensive cells. ### Letting a column take the slack `grow` distributes leftover width. `truncate` and `noWrap` decide what a narrow column does with text that does not fit: clip with a tooltip, or refuse to wrap. ### Remembering the widths Column sizing is a state slice, so adding `columnSizing` to [`syncState`](/react-tablekit/persistence/) is all it takes for a reader's layout to survive a reload. ## Customization `enableResizing: false` exempts one column, and `static: true` exempts it from resizing, reordering, hiding and pinning at once. `autosizeColumn` and `autosizeAllColumns` run the same measurement the double-click uses, so a toolbar button can offer "fit to content". ## Limitations - **Autosizing measures rendered rows only.** On a paginated table it fits the current page, because the other rows are not in the DOM to measure. - **Percentages and resizing do not mix.** Turning resizing on converts them once; after that the table is in pixels. - **`minSize` wins over the available width.** A table whose minimums exceed its container will scroll rather than shrink below them. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableColumnResizing`, `columnResizeMode`, `columnResizeDirection`, `tableLayout`, `minWidth`, `maxWidth`, `onColumnSizingChange`, `onColumnSizingInfoChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `width`, `minWidth`, `maxWidth`, `size`, `minSize`, `maxSize`, `grow`, `enableResizing`, `truncate`, `noWrap`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setColumnSizing`, `resetColumnSizing`, `autosizeColumn`, `autosizeAllColumns`, `getTotalSize`. - [`Column`](/react-tablekit/api/table-instance/): `getSize`, `resetSize`, `getIsResizing`. --- # Column ordering > Drag headers to reorder, or move them from the columns menu with the keyboard. Pinned groups and locked positions are respected. URL: https://react-tablekit.vercel.app/react-tablekit/column-ordering/ ## Basics **Example: Column ordering** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-ordering/demo-basics) ```tsx import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const data = generatePeople(40); const col = createColumnHelper(); const columns = [ // The row's identity: it must stay first and must not be hideable, or the table // becomes a grid of anonymous values. col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name', lockPosition: 'first', enableHiding: false, enableOrdering: false, }), col.accessor('email', { header: 'Email' }), col.accessor('department', { header: 'Department' }), col.accessor('status', { header: 'Status' }), col.accessor('city', { header: 'City' }), col.accessor('age', { header: 'Age', type: 'number' }), col.accessor('joined', { header: 'Joined', type: 'date' }), col.accessor('remote', { header: 'Remote', type: 'boolean' }), ]; /** * Ordering and visibility. Drag a header to move a column, or use the Columns button * to show and hide them. Both are personal preferences rather than something you would share * in a link, so they persist to `localStorage` instead of the URL. Reload and the layout you * arranged is still there. */ export default function ColumnOrderingVisibilityExample() { return ( aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} enableColumnOrdering enableHiding enableColumnActions enableDensityToggle syncState={{ storage: { key: 'react-tablekit:column-layout-demo', version: 1 } }} initialState={{ columnVisibility: { joined: false }, pagination: { pageIndex: 0, pageSize: 8 }, }} /> ); } ``` `enableColumnOrdering` lets a reader drag a header to a new position. The drag uses pointer events rather than HTML5 drag-and-drop, so it behaves the same with a mouse, a pen and a finger. The same order is editable from the columns menu, where each column has a drag handle and up and down buttons, which is how the feature works without a pointer at all. Every move is announced in the live region: _Moved Company name to position 3 of 8_. ### What stays put Reordering respects the layout rules already in place: - A [pinned](/react-tablekit/column-pinning/) column moves only within its own side. - `lockPosition: 'first'` or `'last'` holds a column at its edge. - `enableOrdering: false` takes a column out of the drag entirely, and `static: true` takes it out of everything. ### Reading and setting the order `state.columnOrder` is an array of column ids; anything missing keeps its declared position. That makes a "reset layout" button a one-liner, and a saved layout a stored array. ```tsx ``` `moveColumn(id, toIndex)` performs the same move programmatically. ## Customization Pair ordering with [column visibility](/react-tablekit/column-visibility/) and [sizing](/react-tablekit/column-sizing/), then persist all three; see [Persistence](/react-tablekit/persistence/). Those three slices together are what people mean by "my table layout". ## Limitations - **Group columns move as a unit.** A leaf cannot be dragged out of its header group. - **Order is by id.** Renaming a column id invalidates a stored order, so treat ids as an API. - **There is no drop indicator between two pinned sides.** Dragging across the boundary is refused rather than silently ignored. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableColumnOrdering`, `onColumnOrderChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `enableOrdering`, `lockPosition`, `static`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setColumnOrder`, `resetColumnOrder`, `moveColumn`. - [`Column`](/react-tablekit/api/table-instance/): `getIndex`, `getCanOrder`. --- # Column visibility > A columns menu for the reader, a per-column menu for actions, and responsive hiding that needs no state at all. URL: https://react-tablekit.vercel.app/react-tablekit/column-visibility/ ## Basics **Example: Column features** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-visibility/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(80); /** * Column power features: drag a header to reorder, drag its right edge (or focus it * and press ←/→, Shift for 50px) to resize, and use the “⋮” menu on any header to sort, filter, * pin, hide or autosize that column. The Columns button toggles visibility. */ export default function ColumnFeaturesExample() { return ( aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enableColumnResizing enableColumnOrdering enableColumnActions enableHiding columnResizeMode="onChange" initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` The toolbar's **Columns** button opens a checkbox per column, plus Show all, Hide all and Reset. Past ten columns the menu grows a search box, because scanning thirty checkboxes is not a feature. `defaultHidden` starts a column off, without removing it from the menu: ```tsx col.accessor('internalId', { header: 'Internal id', defaultHidden: true }); col.accessor('audit', { header: 'Audit trail', enableHiding: false }); ``` ### Hiding by screen width `hideBelow` and `hideAbove` are responsive and deliberately _not_ state: a column hidden because the screen is narrow does not show up as unchecked in the menu, because the reader did not uncheck it. ```tsx col.accessor('city', { hideBelow: 'md' }); ``` That is the difference between "there is no room for this" and "I do not want this". ### The column actions menu `enableColumnActions` adds a menu button to each header with sort, filter, group, pin, hide, autosize and reset-size entries: the per-column versions of what the toolbar offers for the whole table. `renderColumnActionsMenuItems` receives the default items, so you can add one, drop one or reorder them rather than rebuilding the menu: ```tsx renderColumnActionsMenuItems={({ column, defaultItems, closeMenu }) => [ ...defaultItems, , ]} ``` The items are React elements: the defaults are the table's own menu items, and yours sit beside them. `closeMenu` closes the menu once your item has done its work. ## Customization The Columns button, the menu and the header menu are all [slots](/react-tablekit/customization/slots/), and `` places the control outside the toolbar. Visibility is a state slice, so [persisting](/react-tablekit/persistence/) `columnVisibility` remembers the reader's choice. ## Limitations - **A hidden column is still computed.** It keeps its filter and its aggregate; hiding is presentation. Remove it from `columns` to stop the work. - **`hideBelow` cannot be overridden by the reader**, by design; there is no checkbox for it. - **Hiding every column** leaves an empty table rather than an error; the menu's Hide all stops one short only when a column sets `enableHiding: false`. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableHiding`, `enableColumnActions`, `renderColumnActionsMenuItems`, `onColumnVisibilityChange`. - [`ColumnDef`](/react-tablekit/api/column-def/): `defaultHidden`, `enableHiding`, `hideBelow`, `hideAbove`, `enableColumnActions`, `static`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setColumnVisibility`, `resetColumnVisibility`, `toggleAllColumnsVisible`, `getIsAllColumnsVisible`, `getVisibleLeafColumns`. - [`Column`](/react-tablekit/api/table-instance/): `toggleVisibility`, `getIsVisible`, `getCanHide`. --- # Column footers > A footer row with per-column totals, computed over the current page or over everything the filters left. URL: https://react-tablekit.vercel.app/react-tablekit/column-footers/ ## Basics **Example: Column footers** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/column-footers/demo-basics) ```tsx import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const data = generatePeople(60); const col = createColumnHelper(); const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }); /** * Column footers. A footer is either a renderer or an aggregate over the filtered rows: * `footerAggregationScope` is what decides whether "total" means this page or the whole result. */ const columns = [ col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name', footer: ({ table }) => `${table.getFilteredRowModel().rows.length} people`, }), col.accessor('department', { header: 'Department', filterVariant: 'select' }), col.accessor('age', { header: 'Age', type: 'number', aggregationFn: 'mean' }), col.accessor('salary', { header: 'Salary', type: 'number', format: (v) => money.format(v), aggregationFn: 'sum', }), ]; export default function ColumnFootersExample() { return ( aria-label="Payroll" data={data} columns={columns} getRowId={(p) => p.id} enableColumnFooters footerAggregationScope="filtered" initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` `enableColumnFooters` renders a `tfoot`. Each column fills its cell either with an aggregate (the same `aggregationFn` [grouping](/react-tablekit/grouping/) uses) or with whatever `footer` returns. ```tsx col.accessor('salary', { type: 'number', aggregationFn: 'sum' }); col.accessor('name', { footer: ({ table }) => table.getFilteredRowModel().rows.length + ' people', }); ``` ### Which rows count `footerAggregationScope` is the whole question a total has to answer. `'filtered'` sums everything the filters left, across pages, the number a reader usually means. `'page'` sums what is on screen, which is what you want when the footer is a per-page subtotal. Filter the demo above and watch the total move: the count follows the filters, not the page. ### Staying visible `enableStickyFooter` keeps the footer against the bottom of the scroll container, so the totals are readable without scrolling to the end of a long table. See [Sticky header and footer](/react-tablekit/sticky-header-and-footer/). ## Customization A footer cell is a normal renderer, so it can hold anything: a formatted currency, a small bar, a link to a report. `formatters` keeps number and date formatting consistent between the body and the footer without repeating an `Intl` instance in every column. ## Limitations - **A footer aggregate needs a value.** Display columns have none, so their footer is whatever `footer` returns or nothing at all. - **Server mode aggregates what it received.** The table cannot total rows it has never seen; return the totals from the API and render them with `footer`. - **There is one footer row.** Two levels of subtotal are grouping, not footers. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableColumnFooters`, `footerAggregationScope`, `enableStickyFooter`, `aggregationFns`, `formatters`. - [`ColumnDef`](/react-tablekit/api/column-def/): `footer`, `aggregationFn`. - [`TableInstance`](/react-tablekit/api/table-instance/): `getFooterGroups`, `getFilteredRowModel`, `getPrePaginationRowModel`. --- # Row pinning > Keep chosen rows at the top or the bottom while the rest of the table sorts, filters and pages away beneath them. URL: https://react-tablekit.vercel.app/react-tablekit/row-pinning/ ## Basics **Example: Row pinning** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/row-pinning/demo-basics) ```tsx import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const data = generatePeople(40); const col = createColumnHelper(); /** * Row pinning. A pinned row stays visible while the rows around it are sorted, filtered * and paged away: the comparison row you keep referring back to. */ const columns = [ col.display({ id: 'pin', header: '', size: 96, static: true, cell: ({ row }) => ( ), }), col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name' }), col.accessor('department', { header: 'Department' }), col.accessor('age', { header: 'Age', type: 'number' }), col.accessor('city', { header: 'City' }), ]; export default function RowPinningExample() { return ( aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} enableRowPinning enableStickyHeader maxHeight={420} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` `enableRowPinning` lets a row be held at the top or the bottom of the table. Sort, filter and page all around it and it stays, which is what makes it useful for comparing one record against the rest. ```tsx ``` Pinning is a row action, not a column, so it goes wherever it belongs in your design: a button in a display column, an entry in a row menu, a keyboard shortcut. ### Staying pinned `keepPinnedRows` is on by default, which means a pinned row survives a filter that would have removed it. Turn it off and pinning is only a reordering: the row keeps its place at the top while it still matches, and disappears when it does not. ### Pinned and sticky With `enableStickyHeader`, top-pinned rows stick under the header and bottom-pinned rows sit above the footer, so they stay on screen in a scrolling table rather than only at the top of the list. ## Customization `getTopRows` and `getBottomRows` return the pinned rows, so a summary line, a count or a "clear pins" button can be driven from the same state. Pinned rows carry `data-pinned`, which is enough for CSS to give them their own treatment without a slot. ## Limitations - **Pinning is keyed by row id**, so it resets on refetch unless `getRowId` is stable. - **A pinned row is rendered twice in spirit, once in fact**: it is lifted out of the body, so it does not also appear in its sorted position. - **Pinned rows count against the page.** A page of ten with two pinned rows shows eight unpinned rows, not ten. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableRowPinning`, `keepPinnedRows`, `enableStickyHeader`, `enableStickyFooter`, `onRowPinningChange`. - [`Row`](/react-tablekit/api/table-instance/): `pin`, `getIsPinned`, `getCanPin`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setRowPinning`, `resetRowPinning`, `getTopRows`, `getBottomRows`, `getCenterRows`. --- # Row numbers > A numbered column that counts through the whole result set, or restarts on every page. URL: https://react-tablekit.vercel.app/react-tablekit/row-numbers/ ## Basics **Example: Row numbers** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/row-numbers/demo-basics) ```tsx import { useState } from 'react'; import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(40); /** * Row numbers. `absolute` counts through the whole result (row 11 is the first row of * page 2), while `relative` restarts at 1 on every page. Page forward to see the difference. */ export default function RowNumbersExample() { const [mode, setMode] = useState<'absolute' | 'relative'>('absolute'); return ( <>
Numbering {(['absolute', 'relative'] as const).map((value) => ( ))}
aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enableRowNumbers rowNumberMode={mode} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` `enableRowNumbers` inserts a narrow display column of ordinals. There are two ways to count, and the difference only shows up on page two: - `rowNumberMode: 'absolute'` (the default) numbers through the result set, so page two starts at 11. Use it when the number is a position: "the 47th slowest query". - `rowNumberMode: 'relative'` restarts at 1 on every page. Use it when the number is a label for talking about what is on screen: "row 3, second from the left". ### What the number is not It is a position, not an identity. Sorting changes it, filtering changes it, and the same record has a different number tomorrow. For anything a person might quote back to you (a ticket reference, an order id), use a real column. ## Customization The column is a display column like any other, so `positionSelectionColumn` and its neighbours decide where it sits relative to the selection and expand columns. Its width and alignment come from the same [tokens](/react-tablekit/customization/tokens/) as the rest, and its header label comes from [localization](/react-tablekit/localization/). If you want something the built-in column does not do (a rank that survives sorting, say), a display column with your own `cell` is a few lines and gives you full control. ## Limitations - **Absolute numbering needs a page size and index**, so in cursor mode, where there is no page number, it counts from the start of the loaded rows. - **The column is excluded from CSV export**, as every display column is. Export the value it derives from instead. - **It is not a rank.** Two rows with the same value get consecutive numbers, not a shared one. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableRowNumbers`, `rowNumberMode`, `positionSelectionColumn`, `positionExpandColumn`, `displayColumnDefs`. - [Localization keys](/react-tablekit/api/localization-keys/): the header label. --- # Row appearance > Striping, hover and per-row classes, styles and props: the way a row shows what it is without a custom renderer. URL: https://react-tablekit.vercel.app/react-tablekit/row-appearance/ ## Basics **Example: Row appearance** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/row-appearance/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(40); /** * Row appearance. Striping and hover are table-wide switches; `getRowClassName` and * `getRowStyle` decorate individual rows from their data. Here, suspended people are dimmed and * remote workers carry an accent border. */ export default function RowAppearanceExample() { return ( aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enableStriped enableHover getRowClassName={(row) => (row.original.status === 'suspended' ? 'is-muted' : undefined)} getRowStyle={(row) => row.original.remote ? { boxShadow: 'inset 3px 0 0 var(--tk-color-accent)' } : undefined } initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` `enableStriped` and `enableHover` are table-wide and tokenized, so they follow the theme rather than hard-coded greys. For anything that depends on the row's data, three callbacks cover almost every case: ```tsx (row.original.overdue ? 'is-overdue' : undefined)} getRowStyle={(row) => (row.original.pinned ? { fontWeight: 600 } : undefined)} getRowProps={(row) => ({ 'data-status': row.original.status })} /> ``` `getRowProps` is the one that reaches furthest: it puts data attributes on the row, which lets CSS do the rest without a class name for every state. ### Interaction `onRowClick`, `onRowDoubleClick` and `onRowContextMenu` make the row itself interactive, and a row only becomes focusable when one of them is set; an unclickable row should not be a tab stop. `onRowMouseEnter` and `onRowMouseLeave` cover hover previews. Clicks that land on a button, a link, an input or an element marked `data-tk-stop` do not count as row clicks, so a row menu keeps working inside a clickable row. ### Rows that are not available `isRowDisabled` marks a row unavailable everywhere at once: it is not selectable, select-all skips it, and it is styled from the disabled tokens. That is one answer instead of three. ## Customization When the callbacks are not enough, `renderRow` replaces the whole row and hands you `defaultRender()` so you can wrap rather than rebuild: ```tsx renderRow={({ row, defaultRender }) => ( {defaultRender()} )} ``` Cell-level work belongs in [Rows and cells](/react-tablekit/customization/rows-and-cells/), and the colours themselves are [tokens](/react-tablekit/customization/tokens/). ## Limitations - **Striping counts rendered rows.** With detail panels or grouping, the alternation follows what is on screen, which is not always the data order. - **`getRowStyle` is inline style**, so it wins over the stylesheet. Prefer a class or a data attribute when a theme should still be able to override it. - **`renderRow` opts out of the table's own row markup**, including its accessibility attributes, so keep `defaultRender()` in the tree unless you are replacing those deliberately. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableStriped`, `enableHover`, `getRowClassName`, `getRowStyle`, `getRowProps`, `renderRow`, `isRowDisabled`, `onRowClick`, `onRowDoubleClick`, `onRowContextMenu`, `onRowMouseEnter`, `onRowMouseLeave`, `onCellClick`, `bordered`, `noWrap`. - [Theme tokens](/react-tablekit/api/theme-tokens/): `--tk-row-bg`, `--tk-row-hover-bg`, `--tk-row-selected-bg`, `--tk-row-stripe-bg`. --- # Sticky header and footer > Keep the header and the totals in view while the rows scroll, in both axes when columns are pinned. URL: https://react-tablekit.vercel.app/react-tablekit/sticky-header-and-footer/ ## Basics **Example: Sticky header and footer** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/sticky-header-and-footer/demo-basics) ```tsx import { createColumnHelper, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; const col = createColumnHelper(); const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }); const columns = [ col.accessor((p) => `${p.firstName} ${p.lastName}`, { id: 'name', header: 'Name' }), col.accessor('department', { header: 'Department', pin: 'left' }), col.accessor('city', { header: 'City' }), col.accessor('age', { header: 'Age', type: 'number', aggregationFn: 'mean', footer: 'Average' }), col.accessor('salary', { header: 'Salary', type: 'number', format: (v) => money.format(v), aggregationFn: 'sum', footer: 'Total', }), ]; const data = generatePeople(200); /** * A bounded, scrollable table: the header sticks to the top of the scroll container, the * footer with its aggregates sticks to the bottom, and the pinned Department column stays put in * both axes. Pagination lives outside the scroll area, so it is always visible. */ export default function StickyHeaderFooterExample() { return ( aria-label="People" data={data} columns={columns} getRowId={(p) => p.id} enableStickyHeader enableStickyFooter enableColumnFooters maxHeight={420} initialState={{ pagination: { pageIndex: 0, pageSize: 50 } }} /> ); } ``` `enableStickyHeader` holds the header cells against the top of the scroll container. `enableStickyFooter` does the same for the `tfoot`. Both need the container to be the thing that scrolls, which means it needs a bounded height: ```tsx ``` `maxHeight` is the usual way to give it one; a parent with a fixed height works just as well. ### Both directions at once With [pinned columns](/react-tablekit/column-pinning/), a pinned header cell is stuck vertically _and_ horizontally: it stays in the corner while the table scrolls both ways. That is one position rule, not two features interfering. ### Pagination stays put The pagination control sits outside the scroll container, so it is always visible. Scrolling to the last row and finding no way to reach page two is a bug this layout cannot have. ## Customization The header background is `--tk-header-bg` and the shadow that appears when content passes underneath is a [token](/react-tablekit/customization/tokens/) as well, so a sticky header can be flat, raised or bordered without touching the markup. Sticky headers and [virtualization](/react-tablekit/virtualization/) compose: the header is outside the virtual window, so it does not move as rows are recycled. ## Limitations - **A sticky header needs a bounded height.** Without one the page scrolls instead of the container and nothing sticks; the table warns about this in development. - **A very tall header group eats the viewport.** Two header rows plus a filter row on a short screen leaves little for data. - **`position: sticky` cannot escape an ancestor with `overflow: hidden`.** If the header will not stick, that ancestor is usually why. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableStickyHeader`, `enableStickyFooter`, `maxHeight`, `minWidth`, `maxWidth`, `layout`, `bordered`. - [Theme tokens](/react-tablekit/api/theme-tokens/): `--tk-header-bg`, `--tk-z-pinned-header`. --- # Surface > The panel the table sits on: a bordered card with its pagination attached, or nothing at all, with corners you choose. URL: https://react-tablekit.vercel.app/react-tablekit/surface/ ## Basics **Example: Surface** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/surface/demo-basics) ```tsx import { useState } from 'react'; import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(40); /** * Surface and corners. `card` wraps the table and its pagination in one panel; `plain` removes it * so the table sits on whatever the page provides, with the pagination as a block beneath. */ export default function SurfaceExample() { const [surface, setSurface] = useState<'card' | 'plain'>('card'); const [rounded, setRounded] = useState(true); return ( <>
Surface {(['card', 'plain'] as const).map((value) => ( ))}
aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} surface={surface} rounded={rounded} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` By default the table and its pagination share one panel: a border, a background and rounded corners, with the seam between them removed so they read as a single card. `surface="plain"` takes the panel away. The table stands on whatever the page already provides, and the pagination becomes a separate block beneath it rather than the bottom of a card. ```tsx ``` That is the one to reach for when the table is already inside a panel of your own (a dialog, a dashboard tile, a section with its own border), where a second frame around it is one frame too many. ### Corners `rounded` switches the outer corners. A card is rounded unless you say otherwise; a plain surface is square unless you ask, because rounding something with no border and no fill only shows where a cell has a background of its own. ```tsx ``` The radius itself is the `--tk-radius` [token](/react-tablekit/customization/tokens/), so `rounded` is a switch rather than a measurement: set the token once and every table that opts in follows the same curve. ### Borders inside the table `bordered` is a different question: it draws the lines _between_ cells rather than around the table. `'outer'` keeps the frame and the row dividers, `'rows'` keeps only the dividers, `'all'` draws a full grid, and `'none'` leaves the table unruled. ## Customization Everything here is a token, so a design system can set the surface once for every table rather than passing props: `--tk-container-border`, `--tk-container-shadow`, `--tk-color-surface` and `--tk-radius` are what `card` is made of. The container is a [slot](/react-tablekit/customization/slots/) as well, so a table that needs to sit inside something more particular than a border can be given one. ## Limitations - **A plain surface still scrolls.** The container element remains, because sticky headers and pinned columns are positioned against it; only its border, fill and shadow are removed. - **`rounded` rounds the outside, not the cells.** Individual cells have no radius; that is a cell renderer's decision. - **The card's seam is only removed for pagination.** A table with `paginationType="none"` and a footer of your own beneath it will show the container's bottom border between them. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `surface`, `rounded`, `bordered`, `maxHeight`, `minWidth`, `maxWidth`, `unstyled`. - [Theme tokens](/react-tablekit/api/theme-tokens/): `--tk-radius`, `--tk-container-border`, `--tk-container-shadow`, `--tk-color-surface`. - [TableSlots](/react-tablekit/api/slots/): `Container`, `Pagination`. --- # Density > Three row heights the reader can switch between (compact, standard and comfortable), driven entirely by tokens. URL: https://react-tablekit.vercel.app/react-tablekit/density/ ## Basics **Example: Density** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/density/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(40); /** * Density changes row height and cell padding only, never the layout. The toolbar * button cycles compact → standard → comfortable; `initialState.density` picks the start. */ export default function DensityExample() { return ( aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enableDensityToggle enableRowSelection initialState={{ density: 'compact', pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` Density changes cell padding and row height, and nothing else. No column moves, no text is hidden, no layout is recomputed, which is why switching it feels instant and why it is safe to let the reader do it. ```tsx ``` `enableDensityToggle` puts the control in the toolbar. Without it, `state.density` still applies, which is useful when density belongs to your own settings screen rather than the table's toolbar. ### Choosing a default `compact` fits roughly a third more rows on a screen and suits dashboards and monitoring views. `standard` is the default. `comfortable` suits touch and long reading sessions, where a taller target matters more than another row. ### Remembering it Density is a state slice, so adding it to [`syncState`](/react-tablekit/persistence/) makes the reader's choice stick across visits. It is one of the slices most worth persisting, because it is a preference rather than a query. ## Customization Each level is a set of [tokens](/react-tablekit/customization/tokens/), so a design system can redefine what "compact" means without forking the component: ```css [data-density='compact'] { --tk-cell-py: 4px; --tk-cell-px: 8px; } ``` `toggleDensity` and `setDensity` drive it from your own control: a keyboard shortcut, a settings menu, a global preference applied to every table at once. ## Limitations - **Density does not change font size.** Text stays legible; only the space around it shrinks. Set `--tk-font-size` yourself if you want both. - **Row height still depends on content.** A cell with two lines is two lines tall at every density. - **With [virtualization](/react-tablekit/virtualization/), changing density re-measures.** Pass the matching `estimateRowHeight` if you set a non-default density up front. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableDensityToggle`, `initialState.density`, `state.density`, `estimateRowHeight`, `onDensityChange`. - [`TableInstance`](/react-tablekit/api/table-instance/): `setDensity`, `toggleDensity`. - [Theme tokens](/react-tablekit/api/theme-tokens/): the `--tk-cell-*` and `--tk-row-height` families. --- # Virtualization > Render the rows on screen instead of all of them: a hundred thousand rows with the same markup, the same keyboard model and the same counts for screen readers. URL: https://react-tablekit.vercel.app/react-tablekit/virtualization/ ## Basics **Example: Virtualization: 100k rows** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/virtualization/demo-basics) ```tsx import { useMemo, useState } from 'react'; import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; /** * 100 000 rows in one scroll container: only the rows near the viewport are in the DOM, * with spacer rows keeping the scrollbar honest. Sorting, searching and filtering still run over * the whole dataset, and the table exposes grid semantics so screen readers get the real totals. */ export default function Virtualization100kExample() { const [count, setCount] = useState(100_000); const data = useMemo(() => generatePeople(count), [count]); return (

Scroll the table: the DOM keeps only the visible window, however far you go.

aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enablePagination={false} enableRowVirtualization enableStickyHeader maxHeight={460} estimateRowHeight={44} />
); } ``` Virtualization renders only the rows in view plus a small overscan, so the cost of a table stops growing with the data. It turns itself on above `virtualizationThreshold` visible rows, which means most tables never think about it and the one that suddenly loads 50,000 rows does not fall over. ```tsx ``` The one thing it needs is a bounded height: `maxHeight`, or a parent that has one. Without it there is no viewport to virtualize against, and the table says so in development. ### Row height `estimateRowHeight` is the starting guess, and rows are measured as they render, so variable heights settle correctly after the first paint. Give a close estimate and the scrollbar is accurate immediately; give a poor one and it corrects itself as you scroll. `overscan` trades memory for smoothness: a few extra rows above and below the viewport mean fewer blank frames during a fast flick. ### Still a real table Virtualized rows use grid semantics: the table reports `aria-rowcount` for the whole result and every row carries its true `aria-rowindex`. A screen reader hears "row 4,201 of 100,000", not "row 12 of 20". [Keyboard navigation](/react-tablekit/keyboard-navigation/) is virtualization-aware too, so Page Down moves by a screen of rows and `scrollToRow` brings any row into view. ### What it composes with Detail panels (measured), grouping, sticky headers, pinned columns and infinite pagination all work under virtualization, which is the point of building it in rather than bolting it on. ## Customization `useVirtualRows` is exported, so a [headless](/react-tablekit/headless/) table or a custom layout can use the same windowing the built-in body uses. `enableRowVirtualization: false` forces it off for a table where every row must be in the DOM, such as a print view, or a test that scrapes markup. ## Limitations - **Browser find-in-page only finds rendered rows.** This is inherent to windowing; offer search in the table instead. - **Columns are not virtualized in 1.0.** Fifty columns render fifty cells per row; column virtualization is on the [roadmap](/react-tablekit/discover-more/roadmap/). - **Wildly variable row heights make the scrollbar approximate** until those rows have been measured, because their size is not known before they render. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableRowVirtualization`, `virtualizationThreshold`, `estimateRowHeight`, `overscan`, `maxHeight`, `layout`. - [Hooks](/react-tablekit/api/hooks/): `useVirtualRows`. - [`TableInstance`](/react-tablekit/api/table-instance/): `scrollToRow`, `getRowCount`. --- # Responsive > Breakpoint-aware props, a card layout for small screens, and a server-rendered first paint that does not jump. URL: https://react-tablekit.vercel.app/react-tablekit/responsive/ ## Basics **Example: Responsive cards** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/responsive/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(40); /** * The cards layout: below the mobile breakpoint each row renders as a card with * label/value pairs instead of a horizontally scrolling table; semantically a list of articles. * Narrow the preview (or your window) below 960px to see it switch. */ export default function ResponsiveCardsExample() { return ( aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} responsive={{ mobileLayout: 'cards', mobileBreakpoint: 'md', cardColumns: ['name', 'email', 'department', 'city', 'salary'], }} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} /> ); } ``` Below the mobile breakpoint a table can stop being a table. `mobileLayout: 'cards'` renders each row as a card of label and value pairs, with the row actions in the card footer and the selection checkbox in its header. Sorting moves to a "Sort by" select, because there are no headers left to click. ```tsx ``` `cardColumns` chooses what a card shows; a card with nine fields is a table with extra steps. ### Scrolling instead The default `mobileLayout: 'scroll'` keeps the table and scrolls it horizontally, with [pinned](/react-tablekit/column-pinning/) columns holding the identifying column in place and tighter padding below the small breakpoint. For a table of numbers that is usually better than cards. ### Responsive props Several props take a value per breakpoint, mobile-first, so behaviour follows the layout without a media query in your code: ```tsx col.display({ id: 'actions', pin: { base: false, md: 'right' } }) ``` `pin`, `headerShort`, the pagination variant, `density` and `maxWidth` all accept one. `headerShort` is the small one worth remembering: a column can be _Monthly recurring revenue_ on a wide screen and _MRR_ on a narrow one. ### Server rendering On the server there is no viewport, so the first render uses `ssrBreakpoint`, which is `lg` by default. Pick the one your typical reader is on and the hydrated layout will match the rendered one instead of snapping into place. ## Customization `renderCard` replaces the card entirely when label-and-value is not the right shape. `useBreakpoint` exposes the same breakpoint state to your own components, so a toolbar button can disappear on mobile using the table's breakpoints rather than a second set of its own. ## Limitations - **Cards have no columns**, so column resizing, reordering, pinning and the header menu do not apply there. - **`hideBelow` and the card layout are different tools.** One removes a column, the other changes the presentation of all of them. - **Breakpoints are the table's, not your framework's.** If your design system already has breakpoints, pass them in so the two agree. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `responsive` (`breakpoints`, `mobileBreakpoint`, `mobileLayout`, `cardColumns`, `renderCard`, `ssrBreakpoint`), `compactPagination`, `maxWidth`, `minWidth`. - [`ColumnDef`](/react-tablekit/api/column-def/): `headerShort`, `hideBelow`, `hideAbove`, `pin`. - [Hooks](/react-tablekit/api/hooks/): `useBreakpoint`. - [`TableInstance`](/react-tablekit/api/table-instance/): `getBreakpoint`. --- # Toolbar > The strip above the table (search, filters, columns, density, export), either as one component or as parts you place yourself. URL: https://react-tablekit.vercel.app/react-tablekit/toolbar/ ## Basics **Example: Composable layout** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/toolbar/demo-basics) ```tsx import { DataTable, useDataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(60); /** * The same table, taken apart. `useDataTable` builds the instance and * `DataTable.Root` shares it; every part below can be moved, wrapped or left out. * Here the search sits in the page header and pagination is above the table. */ export default function ComposableLayoutExample() { const table = useDataTable({ 'aria-label': 'People', data, columns: peopleColumns, getRowId: (p) => p.id, enableRowSelection: true, enableHiding: true, initialState: { pagination: { pageIndex: 0, pageSize: 6 } }, }); return (

Team directory

); } ``` The toolbar assembles itself from the features that are on: search at the start, then the filters button and the active filter chips; density, columns and export at the end. Turn a feature off and its control disappears; there is no separate toolbar configuration to keep in sync. `renderToolbarStart` and `renderToolbarEnd` add your own content beside the built-ins, which is where an **Add** button or a saved-view picker belongs: ```tsx } data={rows} columns={columns} /> ``` ### Taking it apart When the toolbar should not be a strip above the table (because search belongs in the page header, or the filters live in a sidebar), use the composable parts. They talk to the same table instance, so nothing is duplicated: ```tsx const table = useDataTable({ data, columns }); ; ``` `Search`, `FiltersButton`, `FilterPanel`, `ColumnsButton`, `ExportButton`, `Pagination` and `SelectionBar` are all available this way. ### The selection bar When [row selection](/react-tablekit/row-selection/) is on, a second bar appears under the toolbar as soon as something is selected. `renderBulkActions` fills it, and `showSelectionBar: false` removes it if your design puts bulk actions somewhere else. ## Customization Every control is a [slot](/react-tablekit/customization/slots/), so replacing the Filters button with your design system's button is one entry rather than a rebuild. `toolbarActions` is the short form of `renderToolbarEnd` for a list of buttons, and `toolbar: false` removes the strip entirely while leaving the features working. ## Limitations - **The toolbar wraps, it does not collapse.** On a narrow screen the controls flow onto a second line rather than folding into an overflow menu; use the parts if you need one. - **A composable part still needs the table instance.** The parts are not context-free components; they read and write the same state, which is what keeps them consistent. - **Order within the toolbar is fixed.** Start and end slots are yours; the built-ins keep their relative order so tables look alike. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `toolbar`, `toolbarActions`, `renderToolbarStart`, `renderToolbarEnd`, `enableGlobalFilter`, `filterDisplayMode`, `enableHiding`, `enableDensityToggle`, `enableExport`, `showSelectionBar`, `renderBulkActions`. - [TableSlots](/react-tablekit/api/slots/): `Toolbar`, `SearchInput`, `FiltersButton`, `FilterPanel`, `ColumnsButton`, `ExportButton`, `SelectionBar`. - [Hooks](/react-tablekit/api/hooks/): `useDataTable` for the composable form. --- # Loading and empty states > Skeletons, a delayed overlay, an empty state that knows why it is empty, and errors that keep the rest of the table usable. URL: https://react-tablekit.vercel.app/react-tablekit/loading-and-empty-states/ ## Basics **Example: Loading, empty and error states** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/loading-and-empty-states/demo-basics) ```tsx import { useMemo, useState } from 'react'; import { createLocalDataSource, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; type StateName = 'loading' | 'skeleton' | 'refetching' | 'empty' | 'noResults' | 'error'; const people = generatePeople(40); /** * Every data state: the first load as text or skeleton rows, the refetch overlay * that keeps the rows readable, the two empty states (no data at all vs nothing matching the * filters) and the error state with Retry. */ export default function StatesExample() { const [state, setState] = useState('skeleton'); const dataSource = useMemo(() => { const rows = state === 'empty' ? [] : people; return createLocalDataSource(rows, { columns: peopleColumns, getRowId: (p: DemoPerson) => p.id, // "Loading" states never resolve, so the demo can hold them still. latencyMs: state === 'loading' || state === 'skeleton' ? 100_000 : state === 'refetching' ? 1_500 : 0, failRate: state === 'error' ? 1 : 0, }); }, [state]); return (
{state === 'noResults' && (

Search for something that cannot match, e.g. “zzzz”.

)} {state === 'refetching' && (

Change the page to see the overlay.

)}
key={state} aria-label="People" columns={peopleColumns} dataSource={dataSource} getRowId={(p) => p.id} loadingDisplay={state === 'loading' ? 'text' : 'skeleton'} initialState={{ pagination: { pageIndex: 0, pageSize: 8 }, ...(state === 'noResults' ? { globalFilter: 'zzzz-nobody' } : {}), }} />
); } ``` A table spends real time in three states that are not "showing rows", and each has its own answer. **Loading.** `loadingDisplay` chooses between a skeleton (rows of placeholder blocks, sized by `skeletonRowCount`) and an overlay over the rows that are already there. The skeleton suits a first load; the overlay suits a refetch, because keeping the old page visible is less jarring than blanking it. **Empty.** `renderEmptyState` and `emptyStateContent` fill the body when there is nothing to show. The most useful empty state distinguishes "no data yet" from "no matches for these filters", and offers to clear them: ```tsx renderEmptyState={({ table }) => table.getActiveFilterCount() > 0 ? ( ) : (

No people yet.

) } ``` **Error.** `error` and `renderErrorState` show the failure without discarding the table around it, so the reader can change a filter and try again instead of reloading the page. ### The overlay does not flash `loadingOverlayDelayMs` (150ms by default) means a response that arrives quickly never shows a spinner at all. A spinner that appears and vanishes in 80 milliseconds is noise, not feedback. The overlay covers the body from the bottom of the header down, blocks interaction while it is up and sets `aria-busy` on the table. `loadingOverlayBlocksInteraction: false` lets a reader keep typing in the search box while the previous request finishes. ### Both flags `loading` is the first load; `fetching` is a refresh of data that already exists. Using both is what lets a table show a skeleton once and an overlay after that. ## Customization Every state is a [slot](/react-tablekit/customization/slots/): `SkeletonRows`, `LoadingRow`, `LoadingOverlay`, `EmptyState`, `ErrorState` and `ErrorBanner`. A design system replaces them once and every table follows. With `dataSource`, the states are driven for you: `getDataStatus()` reports which one the table is in, and `onStatusChange` fires on every transition. ## Limitations - **An empty page is not an empty table.** Landing on page 5 of a filtered result shows the empty state; `autoResetPageIndex` exists to avoid it. - **The overlay needs a positioned container**, which the table provides, but a custom `LoadingOverlay` slot rendered outside it will not be positioned. - **`error` is display, not recovery.** Retrying is your decision; the table only shows what you give it. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `loading`, `fetching`, `error`, `loadingDisplay`, `skeletonRowCount`, `loadingOverlayDelayMs`, `loadingOverlayBlocksInteraction`, `renderEmptyState`, `emptyStateContent`, `renderErrorState`, `onError`, `onStatusChange`. - [`TableInstance`](/react-tablekit/api/table-instance/): `getDataStatus`, `refresh`, `invalidate`, `getActiveFilterCount`, `clearAllFilters`. - [TableSlots](/react-tablekit/api/slots/): the six state slots. --- # Keyboard navigation > The WAI-ARIA data grid pattern: arrow keys between cells, one tab stop for the whole table, and Enter to reach interactive content. URL: https://react-tablekit.vercel.app/react-tablekit/keyboard-navigation/ ## Basics **Example: Keyboard navigation** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/keyboard-navigation/demo-basics) ```tsx import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(60); /** * Keyboard grid navigation, following the WAI-ARIA "Data Grid" pattern. The whole table * is a single tab stop: Tab enters it and Tab leaves it again, arrows move between cells. */ export default function KeyboardNavigationExample() { return (
Tab
Enter the grid at the last focused cell, then leave it again
← ↑ → ↓
Move between cells, header included
Home / End
First or last cell of the row; with Ctrl, of the whole grid
Page Up / Page Down
Move by one page of rows
Enter
Sort from a header cell, or move into a cell’s buttons and links (Escape returns)
Space
Select the focused row; Shift+Space selects a range
Ctrl/⌘+A
Select every row on the page
aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enableKeyboardNavigation enableRowSelection enableColumnActions initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />
); } ``` `enableKeyboardNavigation` turns the table into a grid in the WAI-ARIA sense: the whole table is one tab stop, and the arrow keys move between cells inside it. Tab in, navigate, Tab out, rather than tabbing through four hundred cells to reach the pagination. | Key | Action | | ------------------- | ------------------------------------------------------------------------------ | | Tab | Enters the grid at the last focused cell, or leaves it | | Arrows | Move between cells, header included | | Home / End | First or last cell in the row; with Ctrl, in the grid | | Page Up / Page Down | Move by a screen of rows | | Enter | Sorts on a header, runs `onRowClick` on a row, or steps into a cell's controls | | Escape | Steps back out of a cell, and closes menus | | Space | Toggles selection of the focused row | | Left / Right | Collapse or expand a row that has children or a panel | ### Reaching what is inside a cell A cell with a link and two buttons is a problem for a grid: the arrow keys own those directions. Enter steps _into_ the cell, where Tab moves between its controls, and Escape steps back out. That is the standard resolution and it is what this implements. ### Without it Turned off, everything is still reachable: buttons, checkboxes, sort buttons and links are all in the tab order. Rows are not focusable unless `onRowClick` makes them interactive; a row you cannot activate should not be a tab stop. ### With virtualization Navigation is [virtualization](/react-tablekit/virtualization/)-aware. Page Down moves by a visible page and scrolls the target row into view, so arrowing past the bottom of the window does not fall off the end of the rendered rows. ## Customization `focusCell` and `scrollToRow` drive focus from your own code: after a save, after an insert, or from a keyboard shortcut of your own. The focus ring is the `--tk-focus-ring` [token](/react-tablekit/customization/tokens/), so it matches the rest of your application rather than the browser default. ## Limitations - **Cell focus is not cell _selection_.** Copying a rectangular range is on the [roadmap](/react-tablekit/discover-more/roadmap/), not in 1.0. - **A cell whose content traps focus will trap it here too.** A custom control that swallows Escape breaks the step-out, and the table cannot intercept that. - **Two grids on one page each have their own tab stop**, which is correct but means the reader tabs twice to cross them. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableKeyboardNavigation`, `onRowClick`, `onCellClick`, `enableRangeSelection`. - [`TableInstance`](/react-tablekit/api/table-instance/): `focusCell`, `scrollToRow`. - [Accessibility](/react-tablekit/guides/accessibility/): how this fits the rest of the accessibility model. --- # Export > CSV of the page, the selection or everything matching, chunked through your API when the result is larger than the browser holds. URL: https://react-tablekit.vercel.app/react-tablekit/export/ ## Basics **Example: Export** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/export/demo-basics) ```tsx import { createLocalDataSource, DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const clientData = generatePeople(120); const serverPeople = generatePeople(3_000, 11); const source = createLocalDataSource(serverPeople, { columns: peopleColumns, getRowId: (p) => p.id, latencyMs: [100, 250], }); /** * CSV export: the toolbar menu exports the current page, every matching row, or just the * selected ones, and can copy to the clipboard instead. Quoting follows RFC 4180 and the file * carries a UTF-8 BOM, so Excel opens accented text correctly. * * In server mode "all matching" is fetched in chunks through the data source, with progress shown * in the button. Try it on the second table, which has 3 000 rows behind a simulated API. */ export default function ExportExample() { return (

Client mode

aria-label="People (client)" data={clientData} columns={peopleColumns} getRowId={(p) => p.id} enableExport enableRowSelection exportFileName="people" initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />

Server mode (chunked)

aria-label="People (server)" columns={peopleColumns} dataSource={source} getRowId={(p) => p.id} enableExport exportMode="all" exportFileName="people-all" initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />
); } ``` `enableExport` adds a toolbar menu with four entries: the current page, everything matching the filters, the selected rows, and copy to clipboard. Three of those are different questions and the menu asks which one you meant rather than guessing. ```tsx ``` ### What lands in the file Headers come from `meta.exportHeader`, then a string `header`, then the column id. Values come from `exportValue`, then `format`, then the value stringified, so an exported date matches the displayed date, and an exported currency is a number rather than `$1,240.00` if you say so. `exportValue` receives the whole row, not the cell value: ```tsx col.accessor('total', { format: (v) => money.format(v), exportValue: (row) => row.total }); ``` Quoting follows RFC 4180, and a UTF-8 byte order mark is written by default so Excel opens accented text correctly. Display columns and `enableExport: false` columns are left out. A field a spreadsheet would run as a formula (one starting with `=`, `+`, `-` or `@`) is written with a leading `'`, so it opens as text. Plain numbers such as `-12` are left alone. The same applies to copying to the clipboard. `escapeFormulas: false` turns it off, for data you trust; see [Security](/react-tablekit/guides/security/). ### Exporting more than the browser has "All matching" in server mode cannot mean the loaded page. The table walks your data source in chunks of `exportChunkSize` up to `exportMaxRows`, showing progress in the button and letting the reader cancel. When your backend already has an export endpoint, `onExport(query, scope)` hands you the query and the table does nothing else, which is the right answer for a million rows. ### Saving the file The default download is a blob and a temporary link. `onExportFile(blob, fileName)` intercepts it for a custom destination: a file-system picker, an upload, a preview dialog. ## Customization `exportToCsv` is exported, so the same serialization is available outside the table for a scheduled report or a test. The export button is a [slot](/react-tablekit/customization/slots/) and `` places it outside the toolbar. ## Limitations - **CSV only in 1.0.** Excel and PDF are the caller's job; `onExport` is the hook for them. - **`exportMaxRows` is a real ceiling.** It defaults to 10,000 so a mis-click cannot pull a million rows through the browser; raise it deliberately. - **What you see is what you get, not what you have.** Hidden columns are excluded: an export is the table as configured, which is usually what the reader means. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `enableExport`, `exportMode`, `exportFileName`, `exportChunkSize`, `exportMaxRows`, `onExport`, `onExportFile`. - [`ColumnDef`](/react-tablekit/api/column-def/): `enableExport`, `exportValue`, `meta.exportHeader`. - [`TableInstance`](/react-tablekit/api/table-instance/): `exportCsv`, `copyToClipboard`, `getQuery`, `getSelectionQuery`. - [Utilities](/react-tablekit/api/utilities/): `exportToCsv`. --- # Persistence > Put table state in the URL, in storage, or in both, so a link reproduces a view and a reload does not lose one. URL: https://react-tablekit.vercel.app/react-tablekit/persistence/ ## Basics **Example: URL and storage sync** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/persistence/demo-basics) ```tsx import { useEffect, useState } from 'react'; import { DataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(120); /** * URL and storage persistence. The page, sort, search and filters live in the URL in a * compact, stable format (`?tk.page=2&tk.sort=name.asc&tk.q=ava`), so a link restores the exact * view; column layout and density go to `localStorage` instead, because they are personal * preferences rather than something you share. * * With a router, pass its search params through `useRouterSync` instead of the History API. */ export default function UrlSyncExample() { const [url, setUrl] = useState(''); useEffect(() => { const update = () => setUrl(window.location.search || '(no parameters yet)'); update(); // The table writes with history.replaceState, which fires no event; poll for the demo. const timer = setInterval(update, 250); return () => clearInterval(timer); }, []); return (

Sort, search, filter or page, then reload: the view comes back. Current URL state:{' '} {url}

aria-label="People" data={data} columns={peopleColumns} getRowId={(p) => p.id} enableHiding enableDensityToggle showActiveFilterChips syncState={{ url: { keys: ['pagination', 'sorting', 'globalFilter', 'columnFilters'] }, storage: { key: 'react-tablekit:url-sync-demo', version: 1 }, }} initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />
); } ``` `syncState` writes chosen state slices somewhere they survive. Two destinations, and the choice between them is about who the state belongs to: ```tsx ``` The **URL** is for what a reader would send to a colleague: the search, the filters, the sort, the page. Paste the link and you get the same view. **Storage** is for what belongs to the reader alone: which columns they keep, how they ordered them, how dense they like their rows. Nobody wants those in a shared link. ### Reading state from a link The table reads the URL on mount, so a link with `?tk.q=ava&tk.sort=age.desc` opens already searched and sorted. The prefix keeps the parameters out of the way of the rest of your query string. ### Storage that outlives a schema change Stored layouts fail badly when the columns change under them. A `version` and a `migrate` function turn that into a handled case: ```tsx storage: { key: 'people-table', version: 2, migrate: (old, version) => (version < 2 ? { ...old, columnOrder: undefined } : old), } ``` Without a migration, a stored payload from an older version is discarded rather than applied: the table would rather start fresh than render a layout that no longer exists. ### Working with your router `useRouterSync` connects the URL sync to a router's own history (React Router, Next.js), so a filter change is a navigation your application knows about rather than a `history.replaceState` behind its back. See [React Router](/react-tablekit/integrations/react-router/). ## Customization `useSyncState` is exported for state that is not the table's: a page's own tab, a sidebar's collapsed flag, kept in the same place with the same encoding. `syncState.storage.storage` accepts any `Storage`-shaped object, which is how you point it at `sessionStorage` or a memory stub in tests. ## Limitations - **URLs have a practical length limit.** A multi-select with fifty values will produce an ugly link; keep large filter sets in storage or on the server. - **Storage is per browser, not per user.** A reader who switches machines starts over; persist server-side if the layout must follow the account. - **A stored page index can point at nothing.** The table clamps it to the last available page rather than showing an empty body. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `syncState`, `initialState`, `state`, `onStateChange`. - [Hooks](/react-tablekit/api/hooks/): `useSyncState`, `useRouterSync`, `useTableState`. - [`TableInstance`](/react-tablekit/api/table-instance/): `getState`, `setState`, `reset`. --- # Headless > The engine without the markup: every feature, none of the table. Use it for cards, lists, charts, or a table you draw yourself. URL: https://react-tablekit.vercel.app/react-tablekit/headless/ ## Basics **Example: Headless** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/headless/demo-basics) ```tsx import { useDataTable } from '@kingtech/react-tablekit'; import { generatePeople, type DemoPerson } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(90); /** * No table markup at all. `useDataTable` is the whole engine (searching, sorting, * selection and pagination), and it renders nothing, so the rows can become anything. Here they * are a grid of cards. * * Note what this example does *not* do: it does not reuse the library's cell components. Those * are part of the table's own rendering and need its context. Headless means reading values off * the instance and deciding the presentation yourself. Here, the column's `format` is applied * so money and dates still read the way they do in a table. */ export default function HeadlessExample() { const table = useDataTable({ 'aria-label': 'People', data, columns: peopleColumns, getRowId: (p) => p.id, enableRowSelection: true, initialState: { pagination: { pageIndex: 0, pageSize: 9 } }, }); const { globalFilter, pagination } = table.getState(); const rows = table.getRowModel().rows; const pageCount = table.getPageCount(); // A card has no header row, so each field carries its own label. const labels = new Map( table.getLeafHeaders().map((header) => { const heading = header.column.columnDef.header; return [header.column.id, typeof heading === 'string' ? heading : header.column.id]; }), ); return (

{table.getRowCount()} people

    {rows.map((row) => (
  • {row.getVisibleCells().map((cell) => { const value: unknown = cell.getValue(); const format = cell.column.columnDef.format as ((v: never, row: DemoPerson) => string) | undefined; // Without a formatter only a primitive is safe to print: anything else would // stringify as "[object Object]", so it reads as missing instead. const printable = (typeof value === 'string' && value !== '') || typeof value === 'number' || typeof value === 'boolean'; const text = format ? format(value as never, row.original) : printable ? String(value) : '—'; return (
    {labels.get(cell.column.id)}
    {text}
    ); })}
  • ))} {rows.length === 0 &&
  • Nobody matches that search.
  • }
); } ``` The features live in a core that knows nothing about the DOM. `useDataTable` gives you that core as a React hook, and what you render is entirely yours: ```tsx const table = useDataTable({ data, columns, getRowId: (p) => p.id }); return (
    {table.getRowModel().rows.map((row) => (
  • {row.getValue('name')}
  • ))}
); ``` Sorting, filtering, search, pagination, grouping, selection and faceting all work; they are computations over rows, not decorations on a `table` element. Only the rendering is missing, because you are supplying it. ### When this is the right shape A list of cards with a sort control. A chart that has to honour the same filters as the table beside it. A virtualized feed. A table whose markup is dictated by a design system that will not accept a component. In all of them the hard part is the data pipeline, and that is exactly what this gives you. ### Outside React `createTable` is the same engine as a plain object, with `subscribe` for change notification. It has no React dependency, which makes it usable in a test, a worker, or another framework's adapter. ### Meeting in the middle You do not have to choose all or nothing. `DataTable.Root` plus the composable parts renders the library's markup where you want it and yours everywhere else; see [Toolbar](/react-tablekit/toolbar/). `useVirtualRows`, `useBreakpoint` and `useSyncState` are all exported for the same reason. ## Customization There is nothing to customize here; that is the point. What the styled table exposes as [slots](/react-tablekit/customization/slots/) and [handlers](/react-tablekit/customization/handlers/), the headless form exposes as ordinary function calls on the instance. ## Limitations - **Accessibility becomes yours.** The roles, `aria-sort`, the live-region announcements and the keyboard model are in the rendering layer you are replacing. - **The stylesheet is not involved**, so [tokens](/react-tablekit/customization/tokens/) and presets do nothing until you use them. - **`flexRender` needs table context.** In a headless tree, read `cell.getValue()` and apply the column's `format` yourself rather than rendering cell contexts outside the table. ## API - [Hooks](/react-tablekit/api/hooks/): `useDataTable`, `useDataSource`, `useTableState`, `useVirtualRows`, `useBreakpoint`, `useSyncState`. - [Utilities](/react-tablekit/api/utilities/): `createTable`, `createColumnHelper`, `createLocalDataSource`, `createRestDataSource`. - [`TableInstance`](/react-tablekit/api/table-instance/): everything the engine exposes. --- # Localization > Every string the table can show, in one object, with number, date and currency formatting that follows the same locale. URL: https://react-tablekit.vercel.app/react-tablekit/localization/ ## Basics **Example: Localization** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/features/localization/demo-basics) ```tsx import { useState } from 'react'; import { DataTable, defaultLocalization, type TableLocalization } from '@kingtech/react-tablekit'; import de from '@kingtech/react-tablekit/locales/de'; import es from '@kingtech/react-tablekit/locales/es'; import hu from '@kingtech/react-tablekit/locales/hu'; import { generatePeople } from '@/mock/data/people'; import { peopleColumns } from '@/demo-support/columns'; const data = generatePeople(80); const LOCALES: Record = { en: { label: 'English', localization: defaultLocalization, locale: 'en-US' }, hu: { label: 'Magyar', localization: hu, locale: 'hu-HU' }, de: { label: 'Deutsch', localization: de, locale: 'de-DE' }, es: { label: 'Español', localization: es, locale: 'es-ES' }, }; /** * Localization: every user-visible string comes from a `TableLocalization` object, and * `locale` drives the `Intl` formatters (number, date and the row range). Locale packs ship as * subpath exports, so you only bundle the ones you import. * * For i18next or similar, pass your own object built from `t()` calls instead of a pack. */ export default function LocalizationExample() { const [lang, setLang] = useState('hu'); const current = LOCALES[lang]!; return (

Open the toolbar menus, sort a column and page through: labels, announcements, the row range and the date and number formats all follow the selection.

p.id} localization={current.localization} locale={current.locale} enableHiding enableDensityToggle enableExport showActiveFilterChips initialState={{ pagination: { pageIndex: 0, pageSize: 8 } }} />
); } ``` No English is hard-coded. Every label, placeholder, menu entry, announcement and empty-state message comes from the `localization` object, and `locale` drives the formatting that goes with it. ```tsx import { hu } from '@kingtech/react-tablekit/locales/hu'; ; ``` `locale` is what `Intl` receives: numbers, currencies, dates and the collator that orders `text`-sorted columns all follow it. Setting the strings without the locale gives you translated labels beside American dates, which is worse than either. ### Changing a few strings The object is merged over the defaults, so overriding one label does not mean supplying all of them: ```tsx ``` That is also how you adjust wording for a domain (_records_ instead of _rows_) without a translation file. ### Values with a count A string that depends on a number can be a function instead of a `{count}` template. It receives the same variables, which is what lets a language with several plural forms be correct: ```tsx localization={{ selectedCount: ({ count }) => (count === 1 ? '1 kiválasztva' : `${count} kiválasztva`), }} ``` ### With an i18n library The object can be built from your existing catalogue, so the table's strings live where the rest of your application's strings live. See [i18next](/react-tablekit/integrations/i18next/). ## Customization Every key is listed on [Localization keys](/react-tablekit/api/localization-keys/) with its English default, so a translation can be checked against a complete list rather than discovered by clicking around. `formatters` handles the values rather than the chrome (the row range, numbers, dates and currencies) and takes the same locale. Right-to-left needs `dir="rtl"`; the layout uses logical properties, so pinning sides and alignment flip on their own. ## Limitations - **The bundled locales are a starting point**, not a guarantee of tone. Review them before shipping to native speakers. - **`locale` does not change the data.** A date stored as a string is formatted only if the column is a `date` column or has a `format`. - **One locale per table.** A page showing two locales needs two tables; the state is per instance. ## API - [`DataTableProps`](/react-tablekit/api/data-table-props/): `locale`, `localization`, `formatters`, `dir`, `icons`. - [Localization keys](/react-tablekit/api/localization-keys/): every key and its default. - [Icons](/react-tablekit/api/icons/): the icon set, replaceable the same way. --- # Demos > Complete application screens, rather than one feature at a time. URL: https://react-tablekit.vercel.app/react-tablekit/demos/ Every other page on this site shows one capability in isolation. These four show the table doing a real job, with the dense `classic` preset, server-backed data and a mock API that has latency and can be made to fail. They are also the pages whose pixels are locked: each has a visual regression baseline at 1440px and 390px, so an accidental layout change fails the build. - [Account list](/react-tablekit/demos/account-list/): server pagination, search in the page header, pinned row actions. - [Asset list](/react-tablekit/demos/asset-list/): a chip list with overflow, clickable chips inside cells. - [Reading history](/react-tablekit/demos/reading-history/): 18 columns, horizontal scroll, per-row disabled actions. - [Asset picker](/react-tablekit/demos/asset-picker/): single selection inside a dialog, sticky header in a bounded scroll. Two interactive tools sit alongside them: - [Playground](/react-tablekit/demos/playground/): every prop as a control, with the code it produces. - [Theme editor](/react-tablekit/demos/theme-editor/): edit tokens live and export a theme. --- # Account list > A server-mode list screen: search in the page header, debounced requests, numbered pages, and the loading, refetch and empty states that go with them. URL: https://react-tablekit.vercel.app/react-tablekit/demos/account-list/ **Example: Account list** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/demos/account-list/demo-basics) ```tsx import { useEffect, useRef, useState } from 'react'; import { DataTable, useDataTable } from '@kingtech/react-tablekit'; import { formatAccountName } from '@/mock/data/accounts'; import type { Account } from '@/mock/types'; import { accountsDataSource } from '@/demo-support/api'; import { ShowcasePage, useShowcaseTheme } from '@/demo-support/ShowcaseFrame'; import { accountColumns, type AccountListMeta } from './basics-columns'; /** * A realistic application screen: server mode against the mock `/api/accounts/search`, with the * search lifted into the page header (300ms debounce, minimum 3 characters, page reset, Ctrl+K), * numbered pagination, the initial loading row, the refetch overlay and an empty state. */ export default function ShowcaseAccountList() { const { theme, colorScheme, toggle } = useShowcaseTheme(); const [editing, setEditing] = useState(null); const meta: AccountListMeta = { onEditAccount: setEditing }; const table = useDataTable({ columns: accountColumns, dataSource: accountsDataSource, getRowId: (a) => a.identifiers.id, enableSorting: false, searchMinLength: 3, searchDebounceMs: 300, searchHotkey: 'mod+k', initialState: { pagination: { pageIndex: 0, pageSize: 10 } }, theme, colorScheme, meta, toolbar: false, 'aria-label': 'account list table', emptyStateContent: 'No accounts found', localization: { loading: 'Loading...', searchPlaceholder: 'Search by name, email, phone or address', }, }); return ( {toggle}

All accounts

setEditing(null)} />
); } function EditDialog({ account, onClose }: { account: Account | null; onClose: () => void }) { const ref = useRef(null); useEffect(() => { const dialog = ref.current; if (!dialog) return; if (account && !dialog.open) dialog.showModal(); if (!account && dialog.open) dialog.close(); }, [account]); return (

Edit {account ? formatAccountName(account) : ''}

A row action opens your own UI; this dialog stands in for an edit form.

); } ``` ## What it shows This is the shape most application screens take: one server-backed list, searched from the page header rather than from inside the table, paged ten at a time. - **The search lives outside the table.** `` sits in the page header and the toolbar is switched off entirely, which is what [composable parts](/react-tablekit/toolbar/) are for. The table still owns the state. - **The request is gated.** A three-character minimum and a 300ms debounce turn typing into one request, and a new search resets to page 1 rather than leaving the reader on page 4 of something else. - **All three data states appear.** The first load shows a loading row, a refetch dims the rows already on screen instead of blanking them, and a search with no results gets an empty state. - **Ctrl+K focuses the box**, scoped to this table. ## How it is built ```tsx const table = useDataTable({ columns: accountColumns, dataSource: accountsDataSource, getRowId: (a) => a.identifiers.id, searchMinLength: 3, searchDebounceMs: 300, searchHotkey: 'mod+k', toolbar: false, }); ``` Everything else is rendering: `DataTable.Root` wraps the page, `DataTable.Search` goes in the header and `DataTable.Table` and `DataTable.Pagination` go where the design says. The theme switch above the table is part of the demo, not the library: it swaps the `classic` preset for `light` so you can see the same screen in two designs. ## Related - [Client and server](/react-tablekit/client-and-server/): the data source behind it. - [Global search](/react-tablekit/global-search/): debouncing, gating and the hotkey. - [Loading and empty states](/react-tablekit/loading-and-empty-states/): the three states. - [Asset list](/react-tablekit/demos/asset-list/): the same screen with interactive cells. --- # Asset list > Interactive content inside a cell: a chip list that collapses past three items, where each chip is a button and the row click still behaves. URL: https://react-tablekit.vercel.app/react-tablekit/demos/asset-list/ **Example: Asset list** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/demos/asset-list/demo-basics) ```tsx import { useEffect, useRef, useState } from 'react'; import { DataTable, useDataTable } from '@kingtech/react-tablekit'; import { formatAccountName } from '@/mock/data/accounts'; import type { Account, Asset } from '@/mock/types'; import { accountsDataSource } from '@/demo-support/api'; import { ShowcasePage, useShowcaseTheme } from '@/demo-support/ShowcaseFrame'; import { assetLabel, assetListColumns, type AssetListMeta } from './basics-columns'; /** * The same server-mode account search, with a chip list in the widest column: at most three * assets are shown and the rest collapse into a `+N` chip. Each chip is a button, which is the * point: a cell can hold interactive content without the row swallowing the click. */ export default function ShowcaseAssetList() { const { theme, colorScheme, toggle } = useShowcaseTheme(); const [inspecting, setInspecting] = useState(null); const [selected, setSelected] = useState(null); const meta: AssetListMeta = { onInspect: setInspecting, onSelectAsset: setSelected }; const table = useDataTable({ columns: assetListColumns, dataSource: accountsDataSource, getRowId: (a) => a.identifiers.id, enableSorting: false, searchMinLength: 3, searchDebounceMs: 300, searchHotkey: 'mod+k', initialState: { pagination: { pageIndex: 0, pageSize: 10 } }, theme, colorScheme, meta, toolbar: false, 'aria-label': 'asset list table', emptyStateContent: 'No accounts found', localization: { loading: 'Loading...', searchPlaceholder: 'Search by name, email, phone or address', }, }); return ( {toggle}

Assets by account

setInspecting(null)} /> setSelected(null)} />
); } function InfoDialog({ title, body, open, onClose, }: { title: string; body: string; open: boolean; onClose: () => void; }) { const ref = useRef(null); useEffect(() => { const dialog = ref.current; if (!dialog) return; if (open && !dialog.open) dialog.showModal(); if (!open && dialog.open) dialog.close(); }, [open]); return (

{title}

{body}

); } ``` ## What it shows The same server-backed account search as the [account list](/react-tablekit/demos/account-list/), with one column doing something harder: showing a variable number of related items without letting the row grow. - **Three chips and a count.** The cell renders up to three assets and collapses the rest into a `+N` chip; the row's Inspect action opens the full list. The row height stays predictable however many assets an account has. - **Each chip is a button.** Clicking one selects that asset and does _not_ trigger the row's own click handler. Interactive descendants are excluded from row clicks, so a cell can hold controls without fighting the row. - **The widest column takes a share, with a floor.** It gets 40% of the width and never less than 200px, so the identifying columns keep theirs. ## How it is built The whole behaviour is one `cell` renderer: ```tsx col.accessor((a) => getAccountAssets(a), { id: 'assets', header: 'Assets', width: '40%', minWidth: '200px', cell: ({ getValue, table }) => ( ( )} /> ), }); ``` `meta` is how a cell reaches the screen around it. The column stays a pure renderer and the page supplies the callbacks, which is what keeps the column reusable across screens. ## Related - [Columns](/react-tablekit/columns/): accessors, `cell` and column widths. - [Rows and cells](/react-tablekit/customization/rows-and-cells/): decorating without replacing. - [Cell building blocks](/react-tablekit/customization/cell-building-blocks/): the primitives these chips are made of. - [Row selection](/react-tablekit/row-selection/): the same interactive-descendant rule. --- # Reading history > A server-sorted time series against a REST endpoint whose parameter names are nothing like the table's, mapped in one place. URL: https://react-tablekit.vercel.app/react-tablekit/demos/reading-history/ **Example: Reading history** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/demos/reading-history/demo-basics) ```tsx import { useState } from 'react'; import { createRestDataSource, DataTable, useDataTable, type DataSource, } from '@kingtech/react-tablekit'; import { mockFetch } from '@/mock/ready'; import type { PagedResponse, Reading } from '@/mock/types'; import { ShowcasePage, useShowcaseTheme } from '@/demo-support/ShowcaseFrame'; import { readingColumns, type ReadingsMeta } from './basics-columns'; const ASSET_ID = 'as_0001'; /** `GET /api/assets/:id/readings` with the listing criteria as query parameters. */ const readingsDataSource: DataSource = createRestDataSource< Reading, PagedResponse >({ url: `/api/assets/${ASSET_ID}/readings`, method: 'GET', mapQuery: (q) => ({ pageNumber: q.pagination.pageIndex, pageSize: q.pagination.pageSize, ...(q.sorting[0] ? { sortColumn: q.sorting[0].id, ascending: !q.sorting[0].desc } : {}), }), mapResult: (r) => ({ rows: r.items, rowCount: r.totalItemCount, query: { pagination: { pageIndex: r.requestCriteria.pageNumber, pageSize: r.requestCriteria.pageSize, }, }, }), // The mock server is a service worker, so requests go through `mockFetch`; a GET carries the // mapped query as search params. fetcher: async (req) => { const params = new URLSearchParams( Object.entries(req.body as Record).map(([k, v]) => [ k, String(v), ]), ); const res = await mockFetch(`${req.url}?${params.toString()}`, { method: req.method, signal: req.signal, }); if (!res.ok) throw new Error(`Request failed: ${res.status}`); return (await res.json()) as PagedResponse; }, }); /** * A deliberately wide table: 18 columns inside a horizontal scroll, sorted on the server, with * pinned right actions whose disabled states follow the row: "Resend" needs a report id and * "View report" needs a URL, and plenty of rows have neither. */ export default function ShowcaseReadings() { const { theme, colorScheme, toggle } = useShowcaseTheme(); const [message, setMessage] = useState(null); const meta: ReadingsMeta = { onResend: (r) => setMessage(`Resending the report of ${r.date}…`), onViewReport: (r) => setMessage(`Opening ${r.reportUrl ?? 'the report'}…`), }; const table = useDataTable({ columns: readingColumns, dataSource: readingsDataSource, getRowId: (r) => r.id, sortingMode: 'server', initialState: { pagination: { pageIndex: 0, pageSize: 10 } }, theme, colorScheme, meta, toolbar: false, enableGlobalFilter: false, 'aria-label': 'reading history table', emptyStateContent: 'No readings found', localization: { loading: 'Loading...' }, }); return ( {toggle}

Reading history

{message ?? 'Pick an action on any row.'}

); } ``` ## What it shows A paged, sorted history from an endpoint that was not designed around this table, which is the normal case. The API wants `pageNumber`, `pageSize`, `sortColumn` and `ascending`; the table produces a `TableQuery`. One adapter reconciles them. - **Sorting happens on the server.** Clicking a header sends a new request, not a local reordering; this is a page of a much longer series. - **The API's names stay the API's names.** `mapQuery` translates outward and `mapResult` translates back, so nothing else in the code has to know about `totalItemCount`. - **The response carries the truth about paging.** The server echoes the criteria it actually applied, and the table adopts that rather than assuming its own request was honoured. ## How it is built ```tsx const readingsDataSource = createRestDataSource>({ url: `/api/assets/${ASSET_ID}/readings`, method: 'GET', mapQuery: (q) => ({ pageNumber: q.pagination.pageIndex, pageSize: q.pagination.pageSize, ...(q.sorting[0] ? { sortColumn: q.sorting[0].id, ascending: !q.sorting[0].desc } : {}), }), mapResult: (r) => ({ rows: r.items, rowCount: r.totalItemCount }), }); ``` That object is the entire integration. The table passes it `dataSource` and everything else (loading, errors, refetch, deduplication) is handled. Column ids are the sort keys the server receives, which is why `sortServerKey` exists for the cases where they should differ. ## Related - [Client and server](/react-tablekit/client-and-server/): the query and the data source. - [Sorting](/react-tablekit/sorting/): server sorting and `sortServerKey`. - [Pagination](/react-tablekit/pagination/): page sizes, totals and unknown counts. - [Server data](/react-tablekit/guides/server-data/): the longer version of this pattern. --- # Asset picker > A table inside a dialog: single selection with radio semantics, row-click to choose, and a client-side dataset small enough to filter instantly. URL: https://react-tablekit.vercel.app/react-tablekit/demos/asset-picker/ **Example: Asset picker** (runs live at https://react-tablekit.vercel.app/embed/react-tablekit/demos/asset-picker/demo-basics) ```tsx import { useMemo, useState } from 'react'; import { ActionButton, createColumnHelper, DataTable, TruncatedText, } from '@kingtech/react-tablekit'; import { generateAccounts, getAccountAssets } from '@/mock/data/accounts'; import type { Asset } from '@/mock/types'; import { PenIcon } from '@/demo-support/icons'; import { ShowcasePage, useShowcaseTheme } from '@/demo-support/ShowcaseFrame'; const col = createColumnHelper(); const units = new Intl.NumberFormat('en-US'); const dateFormat = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); const dash = (v: string | undefined) => v ?? '-'; /** Everything is sortable except the notes and the actions. */ const columns = [ col.accessor('kind', { header: 'Kind', width: '10%', minWidth: '80px' }), col.accessor('capacity', { header: 'Capacity', type: 'number', width: '10%', minWidth: '96px', format: (v) => `${units.format(v)} u/h`, }), col.accessor('enclosure', { header: 'Enclosure', width: '15%', minWidth: '128px', format: dash, }), col.accessor('powerSource', { header: 'Power source', width: '15%', minWidth: '128px', format: dash, }), col.accessor('tier', { header: 'Tier', width: '15%', minWidth: '128px', format: dash }), col.accessor('placement', { header: 'Placement', width: '10%', minWidth: '96px', format: dash }), col.accessor('mounting', { header: 'Mounting', width: '15%', minWidth: '128px', format: dash }), col.accessor('coolingType', { header: 'Cooling', width: '10%', minWidth: '96px', format: dash }), col.accessor('installedAtUTC', { header: 'Installed', type: 'date', width: '15%', minWidth: '128px', format: (v) => (v ? dateFormat.format(new Date(v)) : '-'), }), col.accessor('vendor', { header: 'Vendor', width: '15%', minWidth: '128px', format: dash }), col.accessor('notes', { header: 'Notes', width: '15%', minWidth: '160px', enableSorting: false, cell: ({ getValue }) => , }), col.display({ id: 'actions', header: 'Actions', align: 'right', width: '5%', minWidth: '48px', pin: 'right', static: true, enableSorting: false, cell: () => } label="Edit" onClick={() => undefined} />, }), ]; /** * A picker, as it would appear inside a dialog: client mode throughout (client sorting and * paging), with single selection by row click or radio, and a sticky header inside a bounded * 400px scroll area so the column labels stay visible while you scan. */ export default function ShowcaseAssetPicker() { const { theme, colorScheme, toggle } = useShowcaseTheme(); const [selected, setSelected] = useState>({}); const rows = useMemo(() => generateAccounts(40).flatMap((a) => getAccountAssets(a)), []); const selectedId = Object.keys(selected).find((id) => selected[id]); return ( {toggle}

Select an asset

aria-label="asset selection table" data={rows} columns={columns} getRowId={(a) => a.id} theme={theme} colorScheme={colorScheme} toolbar={false} enableRowSelection enableMultiRowSelection={false} selectOnRowClick state={{ rowSelection: selected }} onRowSelectionChange={(updater) => setSelected((prev) => (typeof updater === 'function' ? updater(prev) : updater)) } enableStickyHeader maxHeight={400} initialState={{ pagination: { pageIndex: 0, pageSize: 10 } }} />

{selectedId ? `Selected: ${selectedId}` : 'Nothing selected yet.'}

); } ``` ## What it shows The other kind of table: not a screen, but a control inside a dialog whose job is to return one value. - **Single selection.** `enableMultiRowSelection: false` turns the checkboxes into radios and removes the header checkbox; there is no "select all" in a picker. - **The row is the target.** `selectOnRowClick` means the reader does not have to hit a 16-pixel radio, while the edit button inside the row still works because interactive descendants are excluded. - **Client mode, deliberately.** The candidate list is small, so it is loaded once and filtered in the browser: no spinner between keystrokes, which matters much more in a dialog than on a page. - **Compact by default.** A dialog is short, so the table starts dense and pages in small chunks. ## How it is built ```tsx a.id} enableRowSelection enableMultiRowSelection={false} selectOnRowClick showSelectionBar={false} initialState={{ density: 'compact', pagination: { pageIndex: 0, pageSize: 8 } }} onRowSelectionChange={(selection) => setChosen(firstId(selection))} /> ``` The selection bar is off because the dialog's own footer holds the confirm and cancel buttons, and two places to act on a selection is one too many. ## Related - [Row selection](/react-tablekit/row-selection/): single, multiple and row-click selection. - [Density](/react-tablekit/density/): why a dialog starts compact. - [Client and server](/react-tablekit/client-and-server/): when loading everything is the right call. - [Cell building blocks](/react-tablekit/customization/cell-building-blocks/): the action button and truncated text used here. --- # Playground > Every prop of DataTable, generated from the types, with a live table and the code for whatever you have configured. URL: https://react-tablekit.vercel.app/react-tablekit/demos/playground/ The controls below are generated from `DataTableProps` itself, so the playground cannot drift from the component: a prop added to the type appears here, and one that is removed disappears. Change anything and the table re-renders with it; the code panel shows exactly the props you have set. _The playground is interactive: it runs in the browser at https://react-tablekit.vercel.app/react-tablekit/demos/playground/ and has no Markdown form._ ## What to try - Turn on **grouping** and then **column footers**, and switch `footerAggregationScope` between `page` and `filtered` to see which total you actually want. - Set **pagination type** to `loadMore` or `infinite` and watch the rows accumulate rather than replace. - Switch **density** and **striping** together: the whole visual system is tokens, so nothing reflows. - Turn off **rows per page** and see the control disappear while paging still works. ## Related - [`DataTableProps`](/react-tablekit/api/data-table-props/): the same props, with their types and documentation. - [Theme editor](/react-tablekit/demos/theme-editor/): the same idea for the visual tokens. - [All features](/react-tablekit/all-features/): a page per capability, each with its own demo. --- # Theme editor > Change any design token and watch the table follow, then copy the CSS or the theme object into your own project. URL: https://react-tablekit.vercel.app/react-tablekit/demos/theme-editor/ Every visual value in the table is a CSS custom property. Change one here and the table below updates immediately: no rebuild, no class names, no specificity fight. When you are happy, copy the result out as CSS or as a theme object. _The theme editor is interactive: it runs in the browser at https://react-tablekit.vercel.app/react-tablekit/demos/theme-editor/ and has no Markdown form._ ## What to try - Change `--tk-color-accent` and notice how far it reaches: the focus ring, the sort indicator, the selected row, the spinner. - Adjust the cell padding tokens and see that this is all `compact` density really is. - Set `--tk-pinned-shadow` to something visible, then scroll a wide table sideways. - Switch the preset and start again from a different set of defaults. ## Related - [Theming](/react-tablekit/customization/theming/): presets, overrides and dark mode. - [Design tokens](/react-tablekit/customization/tokens/): what each token controls. - [Theme tokens](/react-tablekit/api/theme-tokens/): the complete generated list. --- # API reference > Generated reference for every exported symbol, prop, slot, handler, token and key. URL: https://react-tablekit.vercel.app/react-tablekit/api/ Everything in this section is **generated** from the library's own TypeScript declarations and runtime metadata. Nothing here is hand-maintained, which is why it cannot drift from the code. ## Props and types - [DataTableProps](/react-tablekit/api/data-table-props/): every prop of the all-in-one component. - [ColumnDef](/react-tablekit/api/column-def/): the shape of a column. - [TableInstance](/react-tablekit/api/table-instance/): the headless instance and its row, column, cell and header objects. - [TableState](/react-tablekit/api/table-state/): every state slice and the normalized query sent to a server. ## Functions - [Hooks](/react-tablekit/api/hooks/): `useDataTable` and the supporting hooks. - [Utilities](/react-tablekit/api/utilities/): column helper, data sources, CSV export, theming helpers. ## Extension points - [TableSlots](/react-tablekit/api/slots/): every replaceable component. - [Handlers](/react-tablekit/api/handlers/): every overridable interaction. - [Theme tokens](/react-tablekit/api/theme-tokens/): every token, its CSS variable and its value per preset. - [Localization keys](/react-tablekit/api/localization-keys/): every string, with its English default. - [Icons](/react-tablekit/api/icons/): every replaceable icon. --- # DataTableProps > Every prop of the all-in-one component, the view props shared with DataTable.Root, and the imperative handle. URL: https://react-tablekit.vercel.app/react-tablekit/api/data-table-props/ These are the props `` accepts. `DataTableViewProps` is the subset that also applies to `` and the composable parts, and `DataTableHandle` is what a `ref` gives you. ## Reference ### DataTableProps (type) `` props: engine options + view props. | Name | Type | Default | Description | | --- | --- | --- | --- | | `_features?` | `TableFeature[]` | | Advanced: custom features. | | `acknowledgePageLocalFiltering?` | `boolean` | | | | `acknowledgePageLocalSorting?` | `boolean` | | | | `aggregationFns?` | `Record>` | | | | `appendPages?` | `boolean` | | Append each fetched page to the previous ones instead of replacing them. Inferred from a `loadMore` / `infinite` pagination variant; set it explicitly for a custom UI. | | `aria-label?` | `string` | | | | `aria-labelledby?` | `string` | | | | `autoResetExpanded?` | `boolean` | ```ts true ``` | | | `autoResetPageIndex?` | `boolean` | ```ts true in client mode, false with manual pagination ``` | | | `autoResetSelection?` | `boolean` | ```ts false ``` | | | `bordered?` | `"all" \| "none" \| "outer" \| "rows"` | ```ts 'outer' + row dividers ``` | | | `caption?` | `ReactNode` | | Visually hidden unless `showCaption`. | | `className?` | `string` | | | | `classNames?` | `TableClassNames` | | | | `clearSelectionOnQueryChange?` | `boolean` | ```ts false client / true server ``` | | | `colorScheme?` | `"auto" \| "light" \| "dark"` | ```ts 'light' ``` | | | `columnDefaults?` | `Partial>` | | Merged into every column. | | `columnResizeDirection?` | `"ltr" \| "rtl"` | `'ltr'` | | | `columnResizeMode?` | `"onChange" \| "onEnd"` | `'onChange'` | | | `columns?` | `AnyColumnDef[]` | | Column definitions. Required unless `table` is given. | | `compactPagination?` | `CompactPaginationOptions` | | | | `darkTheme?` | `TableTheme` | | | | `data?` | `TData[]` | ```ts [] ``` | Rows (client mode), or the current page (controlled server mode). | | `dataMode?` | `DataMode` | `'client'` (`'server'` when `dataSource` is set) | | | `dataSource?` | `DataSource` | | Server adapter. Implies `dataMode: 'server'`. | | `datePresets?` | `DatePreset[]` | | | | `debugColumns?` | `boolean` | | | | `debugRows?` | `boolean` | | | | `debugTable?` | `boolean` | | | | `dedupeMs?` | `number` | ```ts 0 (in-flight only) ``` | | | `defaultColumn?` | `Partial>` | | Alias of `columnDefaults`. | | `detailPanelProps?` | `object` | | | | `dir?` | `"ltr" \| "rtl"` | | | | `disablePaginationWhileFetching?` | `boolean` | ```ts true ``` | | | `displayColumnDefs?` | `Partial>>>` | | | | `emptyStateContent?` | `ReactNode` | | Shortcut for a static empty state (e.g. a precondition message). | | `enableColumnActions?` | `boolean` | ```ts false ``` | | | `enableColumnFilters?` | `boolean` | ```ts true ``` | | | `enableColumnFooters?` | `boolean` | | | | `enableColumnOrdering?` | `boolean` | ```ts false ``` | | | `enableColumnPinning?` | `boolean` | ```ts true ``` | | | `enableColumnResizing?` | `boolean` | ```ts false ``` | | | `enableDensityToggle?` | `boolean` | | | | `enableExpandAll?` | `boolean` | ```ts true in multiple expand mode ``` | | | `enableExpanding?` | `boolean` | | Auto when `getSubRows`, `renderDetailPanel` or `getRowCanExpand` is set. | | `enableExport?` | `boolean` | | | | `enableFacetedValues?` | `boolean` | ```ts true ``` | | | `enableFilters?` | `boolean` | ```ts true ``` | Master switch for all filtering. | | `enableGlobalFilter?` | `boolean` | ```ts true ``` | | | `enableGrouping?` | `boolean` | ```ts false ``` | | | `enableHiding?` | `boolean` | ```ts true ``` | | | `enableHover?` | `boolean` | ```ts true (false in classic) ``` | | | `enableKeyboardNavigation?` | `boolean` | | | | `enableMultiRowSelection?` | `boolean \| function` | ```ts true ``` | `false` = single selection. | | `enableMultiSort?` | `boolean` | ```ts true ``` | | | `enablePagination?` | `boolean` | ```ts true ``` | | | `enableRangeSelection?` | `boolean` | ```ts true ``` | | | `enableRowNumbers?` | `boolean` | | | | `enableRowPinning?` | `boolean \| function` | ```ts false ``` | | | `enableRowSelection?` | `boolean \| function` | ```ts false ``` | | | `enableRowVirtualization?` | `boolean \| "auto"` | ```ts 'auto' ``` | | | `enableSorting?` | `boolean` | ```ts true ``` | | | `enableSortingRemoval?` | `boolean` | ```ts true ``` | | | `enableStickyFooter?` | `boolean` | | | | `enableStickyHeader?` | `boolean` | | | | `enableStriped?` | `boolean` | | | | `enableSubRowSelection?` | `boolean \| function` | ```ts true ``` | | | `error?` | `unknown` | | | | `estimateRowHeight?` | `number \| function` | ```ts by density ``` | Row height estimate for virtualization, in px. | | `expandMode?` | `"multiple" \| "single"` | `'multiple'` | | | `expandOnRowClick?` | `boolean` | | | | `exportChunkSize?` | `number` | ```ts 1000 ``` | Rows per chunk for server "export all". | | `exportFileName?` | `string` | | | | `exportMaxRows?` | `number` | ```ts 10000 ``` | Cap for server "export all". | | `exportMode?` | `"all" \| "page"` | ```ts 'page' ``` | | | `facetingMode?` | `DataMode` | | | | `facets?` | `Record` | | Server facets in controlled server mode (with `manualFaceting`), by column id. | | `fetching?` | `boolean` | | Refetch with existing data (overlay). | | `fetchOnMount?` | `boolean` | ```ts true ``` | | | `filterApplyMode?` | `"instant" \| "manual"` | ```ts 'instant' ``` | | | `filterDebounceMs?` | `number` | ```ts 300 ``` | | | `filterDisplayMode?` | `"none" \| "popover" \| "row" \| "panel"` | ```ts 'panel' ``` | | | `filterFns?` | `Record>` | | | | `filterFromLeafRows?` | `boolean` | ```ts false ``` | | | `filterMode?` | `DataMode` | | | | `firstColumnAsRowHeader?` | `boolean` | ```ts true ``` | First data cell rendered as ``. | | `footerAggregationScope?` | `"page" \| "filtered"` | `'filtered'` | | | `formatters?` | `Partial` | | | | `getColumnCanGlobalFilter?` | `function` | | Decides per column whether the global search looks at it. | | `getRowCanExpand?` | `function` | | Decides per row whether it can expand (lazy children that are not loaded yet). | | `getRowCanSelect?` | `function` | | Alias of the function form of `enableRowSelection`. | | `getRowClassName?` | `function` | | Extra class names per row. | | `getRowId?` | `function` | ```ts index-based ``` | Strongly recommended; used for keys, selection and expansion. | | `getRowProps?` | `function` | | Extra attributes and events for each ``. | | `getRowStyle?` | `function` | | Inline styles per row. | | `getSubRows?` | `function` | | Tree data (client mode). | | `globalFilterFn?` | `FilterFnName \| FilterFn` | `'includesString'` | | | `globalFilterMatch?` | `"all-words" \| "any-word" \| "phrase"` | `'all-words'` | | | `groupedColumnMode?` | `false \| "reorder" \| "remove"` | `'reorder'` | | | `groupingMode?` | `DataMode` | | | | `handlers?` | `Partial>` | | | | `highlightSearchMatches?` | `boolean` | ```ts false ``` | Wraps global-search matches in `` in text cells rendered by the default cell renderer. | | `icons?` | `Partial` | | | | `id?` | `string` | ```ts useId() ``` | Base id for aria relationships. | | `initialState?` | `InitialTableState` | | | | `isMultiSortEvent?` | `function` | ```ts Shift+click ``` | Decides whether a click adds to the sort list. | | `isRowDisabled?` | `function` | | Disabled rows are dimmed, not selectable/clickable. | | `keepPinnedRows?` | `boolean` | ```ts true ``` | | | `keepPreviousData?` | `boolean` | ```ts true ``` | | | `layout?` | `"table" \| "grid"` | ```ts 'table' (auto 'grid' when virtualized) ``` | | | `loading?` | `boolean` | | Initial load (no data yet). | | `loadingDisplay?` | `"text" \| "skeleton" \| "spinner"` | ```ts 'skeleton' ('text' in classic) ``` | | | `loadingOverlayBlocksInteraction?` | `boolean` | ```ts true ``` | | | `loadingOverlayDelayMs?` | `number` | ```ts 150 (0 in classic) ``` | | | `locale?` | `string` | `navigator.language` / `'en-US'` | BCP-47 locale for collation/formatting. | | `localization?` | `Partial` | | | | `manualExpanding?` | `boolean` | | | | `manualFaceting?` | `boolean` | | | | `manualFiltering?` | `boolean` | | | | `manualGrouping?` | `boolean` | | | | `manualPagination?` | `boolean` | | | | `manualSorting?` | `boolean` | | | | `maxHeight?` | `string \| number` | | Scroll container max-height (enables inner scroll). | | `maxLeafRowFilterDepth?` | `number` | ```ts Infinity ``` | | | `maxMultiSortColCount?` | `number` | ```ts 3 ``` | | | `maxWidth?` | `ResponsiveValue` | ```ts '100%' ``` | | | `mergeOptions?` | `function` | | Advanced: customizes how new options are merged into the instance on each render. | | `meta?` | `TableMeta` | | | | `minWidth?` | `string \| number` | ```ts 650 in classic ``` | Table min-width. | | `multiSelectDisplay?` | `"list" \| "autocomplete"` | ```ts 'list' ``` | | | `noWrap?` | `boolean` | ```ts true ``` | `white-space: nowrap` in cells. | | `onCellClick?` | `function` | | Called when a single cell is clicked. | | `onColumnFiltersChange?` | `OnChangeFn` | | | | `onColumnOrderChange?` | `OnChangeFn` | | | | `onColumnPinningChange?` | `OnChangeFn` | | | | `onColumnSizingChange?` | `OnChangeFn` | | | | `onColumnSizingInfoChange?` | `OnChangeFn` | | | | `onColumnVisibilityChange?` | `OnChangeFn` | | | | `onDensityChange?` | `OnChangeFn` | | | | `onError?` | `function` | | Called when a data-source request fails, for logging or a toast. | | `onExpandedChange?` | `OnChangeFn` | | | | `onExport?` | `function` | | Delegates the export to your backend instead of generating CSV in the browser. | | `onExportFile?` | `function` | | Takes over saving the generated file (a custom download, a share sheet…). | | `onGlobalFilterChange?` | `OnChangeFn` | | | | `onGroupingChange?` | `OnChangeFn` | | | | `onPaginationChange?` | `OnChangeFn` | | | | `onQueryChange?` | `function` | | Called whenever the normalized query changes, with the reason. | | `onRowClick?` | `function` | | Called when a row is clicked (clicks on interactive content are ignored). | | `onRowContextMenu?` | `function` | | Called on a right click / long press, for a custom context menu. | | `onRowDoubleClick?` | `function` | | Called on a double click anywhere in the row. | | `onRowMouseEnter?` | `function` | | Called when the pointer enters the row. | | `onRowMouseLeave?` | `function` | | Called when the pointer leaves the row. | | `onRowPinningChange?` | `OnChangeFn` | | | | `onRowSelectionChange?` | `OnChangeFn` | | | | `onSortingChange?` | `OnChangeFn` | | | | `onStateChange?` | `OnChangeFn` | | | | `onStatusChange?` | `function` | | Called whenever the loading/fetching/error status changes. | | `overscan?` | `number` | ```ts 8 ``` | | | `pageCount?` | `number` | | | | `paginateExpandedRows?` | `boolean` | ```ts true ``` | | | `pagination?` | `PaginationDisplayOptions` | | | | `paginationMode?` | `DataMode` | | | | `paginationType?` | `"cursor" \| "offset"` | `'offset'` | | | `positionActionsColumn?` | `"first" \| "last"` | ```ts 'last' ``` | | | `positionExpandColumn?` | `"first" \| "last" \| "none"` | ```ts 'first' ``` | | | `positionSelectionColumn?` | `"first" \| "last"` | ```ts 'first' ``` | | | `refetchInterval?` | `number` | | | | `refetchOnWindowFocus?` | `boolean` | ```ts false ``` | | | `renderBulkActions?` | `function` | | Buttons shown in the selection bar while rows are selected. | | `renderColumnActionsMenuItems?` | `function` | | Adds to, reorders or replaces the entries of a column's actions menu. | | `renderDetailPanel?` | `function` | | Collapsible content rendered in a full-width row under the expanded row. | | `renderEmptyState?` | `function` | | Replaces the empty state; `reason` distinguishes "no rows" from "no results". | | `renderErrorState?` | `function` | | Replaces the error state shown when loading failed with no rows to fall back on. | | `renderFallbackValue?` | `Renderable` | | Table-wide fallback for empty values. | | `renderRow?` | `function` | | Replaces or wraps the whole row; call `defaultRender()` to keep the default markup. | | `renderRowActions?` | `function` | | Action buttons for each row; they go into an auto-inserted actions column. | | `renderSubComponent?` | `function` | | Alias of `renderDetailPanel`. | | `renderToolbarEnd?` | `function` | | Replaces the trailing toolbar group (density, columns, export). | | `renderToolbarStart?` | `function` | | Replaces the leading toolbar group (search, filters, chips). | | `resetPageOn?` | `ResetPageReason[]` | ```ts every reason ``` | | | `responsive?` | `ResponsiveOptions` | | | | `rounded?` | `boolean` | ```ts true on a card, false on a plain surface ``` | Round the table's outer corners. The radius itself is the `--tk-radius` token, so this is a switch rather than a measurement. | | `rowActionsColumn?` | `Partial> & object` | | | | `rowCount?` | `number` | | Total rows in server mode (`-1` = unknown). | | `rowNumberMode?` | `"absolute" \| "relative"` | ```ts 'absolute' ``` | | | `scrollToTopOnPageChange?` | `boolean` | ```ts true ``` | | | `searchDebounceMs?` | `number` | ```ts 300 ``` | | | `searchHotkey?` | `string \| false` | ```ts false ``` | e.g. `'mod+k'`, scoped to this table. | | `searchMinLength?` | `number` | ```ts 0 ``` | | | `searchMode?` | `DataMode` | | | | `searchPlaceholder?` | `string` | | | | `selectAllMode?` | `"all" \| "page"` | `'page'` | | | `selectOnRowClick?` | `boolean` | | | | `showActiveFilterChips?` | `boolean` | ```ts true ``` | | | `showCaption?` | `boolean` | | | | `showFacetCounts?` | `boolean` | ```ts true ``` | | | `showSearchHotkeyHint?` | `boolean` | | | | `showSelectionBar?` | `boolean` | ```ts true with multi-selection ``` | | | `skeletonRowCount?` | `number` | ```ts pageSize ``` | | | `slotProps?` | `TableSlotProps` | | | | `slots?` | `Partial>` | | | | `sortDescFirst?` | `boolean` | ```ts false ``` | | | `sortingFns?` | `Record>` | | | | `sortingMode?` | `DataMode` | | | | `state?` | `Partial` | | | | `stickyActions?` | `boolean` | | Convenience alias: pins the actions column right. | | `style?` | `CSSProperties` | | | | `styles?` | `TableStyles` | | | | `surface?` | `"card" \| "plain"` | ```ts 'card' ``` | The surface the table sits on. `'card'` wraps the table and its pagination in one bordered, filled panel. `'plain'` removes that panel: the table stands directly on the page and the pagination becomes a separate block beneath it, which is what you want when the page already provides the surface. | | `syncState?` | `SyncStateOptions` | | | | `table?` | `TableInstance` | | Render an instance created with `useDataTable`. | | `tableLayout?` | `"auto" \| "fixed"` | ```ts 'auto' ``` | | | `tableRef?` | `function` | | Receives the table instance, for imperative access from outside the component. | | `theme?` | `TableTheme \| object` | | | | `toolbar?` | `boolean \| "top" \| "bottom" \| "both"` | ```ts 'top' when any toolbar feature is enabled ``` | | | `toolbarActions?` | `ReactNode \| function` | | Custom content for the end of the toolbar (e.g. an "Add customer" button). | | `treeColumnId?` | `string` | | | | `unstyled?` | `boolean` | | Drops the visual theme (structural CSS only). | | `virtualizationThreshold?` | `number` | ```ts 200 ``` | | ### DataTableViewProps (interface) View-only props. | Name | Type | Default | Description | | --- | --- | --- | --- | | `aria-label?` | `string` | | | | `aria-labelledby?` | `string` | | | | `bordered?` | `"all" \| "none" \| "outer" \| "rows"` | ```ts 'outer' + row dividers ``` | | | `caption?` | `ReactNode` | | Visually hidden unless `showCaption`. | | `className?` | `string` | | | | `classNames?` | `TableClassNames` | | | | `colorScheme?` | `"auto" \| "light" \| "dark"` | ```ts 'light' ``` | | | `compactPagination?` | `CompactPaginationOptions` | | | | `darkTheme?` | `TableTheme` | | | | `datePresets?` | `DatePreset[]` | | | | `detailPanelProps?` | `object` | | | | `dir?` | `"ltr" \| "rtl"` | | | | `disablePaginationWhileFetching?` | `boolean` | ```ts true ``` | | | `displayColumnDefs?` | `Partial>>>` | | | | `emptyStateContent?` | `ReactNode` | | Shortcut for a static empty state (e.g. a precondition message). | | `enableColumnFooters?` | `boolean` | | | | `enableDensityToggle?` | `boolean` | | | | `enableExpandAll?` | `boolean` | ```ts true in multiple expand mode ``` | | | `enableExport?` | `boolean` | | | | `enableHover?` | `boolean` | ```ts true (false in classic) ``` | | | `enableKeyboardNavigation?` | `boolean` | | | | `enableRangeSelection?` | `boolean` | ```ts true ``` | | | `enableRowNumbers?` | `boolean` | | | | `enableRowVirtualization?` | `boolean \| "auto"` | ```ts 'auto' ``` | | | `enableStickyFooter?` | `boolean` | | | | `enableStickyHeader?` | `boolean` | | | | `enableStriped?` | `boolean` | | | | `estimateRowHeight?` | `number \| function` | ```ts by density ``` | Row height estimate for virtualization, in px. | | `expandOnRowClick?` | `boolean` | | | | `exportFileName?` | `string` | | | | `exportMode?` | `"all" \| "page"` | ```ts 'page' ``` | | | `filterApplyMode?` | `"instant" \| "manual"` | ```ts 'instant' ``` | | | `filterDisplayMode?` | `"none" \| "popover" \| "row" \| "panel"` | ```ts 'panel' ``` | | | `firstColumnAsRowHeader?` | `boolean` | ```ts true ``` | First data cell rendered as ``. | | `formatters?` | `Partial` | | | | `getRowClassName?` | `function` | | Extra class names per row. | | `getRowProps?` | `function` | | Extra attributes and events for each ``. | | `getRowStyle?` | `function` | | Inline styles per row. | | `handlers?` | `Partial>` | | | | `highlightSearchMatches?` | `boolean` | ```ts false ``` | Wraps global-search matches in `` in text cells rendered by the default cell renderer. | | `icons?` | `Partial` | | | | `id?` | `string` | ```ts useId() ``` | Base id for aria relationships. | | `layout?` | `"table" \| "grid"` | ```ts 'table' (auto 'grid' when virtualized) ``` | | | `loadingDisplay?` | `"text" \| "skeleton" \| "spinner"` | ```ts 'skeleton' ('text' in classic) ``` | | | `loadingOverlayBlocksInteraction?` | `boolean` | ```ts true ``` | | | `loadingOverlayDelayMs?` | `number` | ```ts 150 (0 in classic) ``` | | | `localization?` | `Partial` | | | | `maxHeight?` | `string \| number` | | Scroll container max-height (enables inner scroll). | | `maxWidth?` | `ResponsiveValue` | ```ts '100%' ``` | | | `minWidth?` | `string \| number` | ```ts 650 in classic ``` | Table min-width. | | `multiSelectDisplay?` | `"list" \| "autocomplete"` | ```ts 'list' ``` | | | `noWrap?` | `boolean` | ```ts true ``` | `white-space: nowrap` in cells. | | `onCellClick?` | `function` | | Called when a single cell is clicked. | | `onExport?` | `function` | | Delegates the export to your backend instead of generating CSV in the browser. | | `onExportFile?` | `function` | | Takes over saving the generated file (a custom download, a share sheet…). | | `onRowClick?` | `function` | | Called when a row is clicked (clicks on interactive content are ignored). | | `onRowContextMenu?` | `function` | | Called on a right click / long press, for a custom context menu. | | `onRowDoubleClick?` | `function` | | Called on a double click anywhere in the row. | | `onRowMouseEnter?` | `function` | | Called when the pointer enters the row. | | `onRowMouseLeave?` | `function` | | Called when the pointer leaves the row. | | `overscan?` | `number` | ```ts 8 ``` | | | `pagination?` | `PaginationDisplayOptions` | | | | `positionActionsColumn?` | `"first" \| "last"` | ```ts 'last' ``` | | | `positionExpandColumn?` | `"first" \| "last" \| "none"` | ```ts 'first' ``` | | | `positionSelectionColumn?` | `"first" \| "last"` | ```ts 'first' ``` | | | `renderBulkActions?` | `function` | | Buttons shown in the selection bar while rows are selected. | | `renderColumnActionsMenuItems?` | `function` | | Adds to, reorders or replaces the entries of a column's actions menu. | | `renderDetailPanel?` | `function` | | Collapsible content rendered in a full-width row under the expanded row. | | `renderEmptyState?` | `function` | | Replaces the empty state; `reason` distinguishes "no rows" from "no results". | | `renderErrorState?` | `function` | | Replaces the error state shown when loading failed with no rows to fall back on. | | `renderRow?` | `function` | | Replaces or wraps the whole row; call `defaultRender()` to keep the default markup. | | `renderRowActions?` | `function` | | Action buttons for each row; they go into an auto-inserted actions column. | | `renderSubComponent?` | `function` | | Alias of `renderDetailPanel`. | | `renderToolbarEnd?` | `function` | | Replaces the trailing toolbar group (density, columns, export). | | `renderToolbarStart?` | `function` | | Replaces the leading toolbar group (search, filters, chips). | | `responsive?` | `ResponsiveOptions` | | | | `rounded?` | `boolean` | ```ts true on a card, false on a plain surface ``` | Round the table's outer corners. The radius itself is the `--tk-radius` token, so this is a switch rather than a measurement. | | `rowActionsColumn?` | `Partial> & object` | | | | `rowNumberMode?` | `"absolute" \| "relative"` | ```ts 'absolute' ``` | | | `scrollToTopOnPageChange?` | `boolean` | ```ts true ``` | | | `searchHotkey?` | `string \| false` | ```ts false ``` | e.g. `'mod+k'`, scoped to this table. | | `searchPlaceholder?` | `string` | | | | `selectOnRowClick?` | `boolean` | | | | `showActiveFilterChips?` | `boolean` | ```ts true ``` | | | `showCaption?` | `boolean` | | | | `showFacetCounts?` | `boolean` | ```ts true ``` | | | `showSearchHotkeyHint?` | `boolean` | | | | `showSelectionBar?` | `boolean` | ```ts true with multi-selection ``` | | | `skeletonRowCount?` | `number` | ```ts pageSize ``` | | | `slotProps?` | `TableSlotProps` | | | | `slots?` | `Partial>` | | | | `style?` | `CSSProperties` | | | | `styles?` | `TableStyles` | | | | `surface?` | `"card" \| "plain"` | ```ts 'card' ``` | The surface the table sits on. `'card'` wraps the table and its pagination in one bordered, filled panel. `'plain'` removes that panel: the table stands directly on the page and the pagination becomes a separate block beneath it, which is what you want when the page already provides the surface. | | `syncState?` | `SyncStateOptions` | | | | `tableLayout?` | `"auto" \| "fixed"` | ```ts 'auto' ``` | | | `tableRef?` | `function` | | Receives the table instance, for imperative access from outside the component. | | `theme?` | `TableTheme \| object` | | | | `toolbar?` | `boolean \| "top" \| "bottom" \| "both"` | ```ts 'top' when any toolbar feature is enabled ``` | | | `toolbarActions?` | `ReactNode \| function` | | Custom content for the end of the toolbar (e.g. an "Add customer" button). | | `treeColumnId?` | `string` | | | | `unstyled?` | `boolean` | | Drops the visual theme (structural CSS only). | | `virtualizationThreshold?` | `number` | ```ts 200 ``` | | ### DataTableHandle (interface) The imperative handle of ``. | Name | Type | Default | Description | | --- | --- | --- | --- | | `focus` | `() => void` | | Moves focus into the table (its first interactive cell, or the container). | | `scrollToRow` | `(id: string, opts?: object) => void` | | Scrolls one row into view. | | `scrollToTop` | `() => void` | | Scrolls the table's scroll container back to the top. | | `table` | `TableInstance` | | The underlying table instance. | ## Related - [Columns](/react-tablekit/columns/) - [Client and server](/react-tablekit/client-and-server/) - [Playground](/react-tablekit/demos/playground/) --- # ColumnDef > The shape of a column: accessors, rendering, types, sizing, pinning, filtering, grouping and export. URL: https://react-tablekit.vercel.app/react-tablekit/api/column-def/ A column definition is one object. The variants differ only in where the value comes from: an accessor key, an accessor function, nothing at all (a display column), or nested `columns` (a group column). ## Writing the type down `TValue` is inferred by `createColumnHelper`, so `cell` and `format` see the narrowed value. That is also why `ColumnDef` is the wrong annotation for an array of them: it defaults `TValue` to `unknown`, and a helper-built column will not assign to it. Use `AnyColumnDef`, which is what the `columns` prop takes: ```ts import type { AnyColumnDef } from '@kingtech/react-tablekit'; export const columns: AnyColumnDef[] = [ col.accessor('name', { header: 'Name' }), col.accessor('score', { header: 'Score', format: (v) => v.toFixed(1) }), ]; ``` If the array stays in the same module as the table, let it infer and skip the annotation. ## Reference ### ColumnDef (variable) ### ColumnDefBase (interface) Fields shared by every kind of column definition. | Name | Type | Default | Description | | --- | --- | --- | --- | | `aggregatedCell?` | `ColumnTemplate>` | | | | `aggregationFn?` | `AggregationFnName \| AggregationFn` | | | | `align?` | `ColumnAlign` | `'left'`; `'right'` for `type: 'number'` | | | `cell?` | `ColumnTemplate>` | | Cell content. Defaults to the formatted value. | | `cellClassName?` | `string \| function` | | Class names for this column's body cells. | | `cellStyle?` | `Record \| function` | | Inline styles for this column's body cells. | | `defaultHidden?` | `boolean` | | Initial visibility. | | `enableColumnActions?` | `boolean` | | | | `enableColumnFilter?` | `boolean` | | | | `enableExport?` | `boolean` | ```ts true ``` | | | `enableGlobalFilter?` | `boolean` | `true` for accessor columns | | | `enableGrouping?` | `boolean` | | | | `enableHiding?` | `boolean` | ```ts true ``` | `false` = always visible (not in the columns menu). | | `enableOrdering?` | `boolean` | ```ts true ``` | `false` = can't be reordered. | | `enableResizing?` | `boolean` | ```ts true ``` | | | `enableSorting?` | `boolean` | the table's `enableSorting` | Whether this column can be sorted. | | `exportValue?` | `function` | | The value written to CSV, when it differs from what is displayed. | | `filterFn?` | `FilterFnName \| FilterFn` | | | | `filterOperators?` | `FilterOperator[]` | | | | `filterOptions?` | `FilterOption[] \| function` | | Values offered by the filter control; a function or promise loads them lazily. | | `filterPlaceholder?` | `string` | | | | `filterServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `columnFilters[].id`. | | `filterVariant?` | `FilterVariant` | | | | `footer?` | `ColumnTemplate>` | | Footer content. | | `format?` | `function` | | Display string (also used by search and export). | | `getCellProps?` | `function` | | Extra attributes and events for this column's cells. | | `getGroupingValue?` | `function` | | The value rows are grouped by, when it differs from the cell value. | | `getSearchValue?` | `function` | `format(value)` | What global search matches against. | | `groupedCell?` | `ColumnTemplate>` | | | | `grow?` | `number \| boolean` | | Flex-grow share in grid layout. | | `header?` | `ColumnTemplate>` | | Header content. Defaults to the humanized id. | | `headerAlign?` | `ColumnAlign` | the column's `align` | | | `headerClassName?` | `string \| function` | | Class names for this column's header cell. | | `headerShort?` | `ResponsiveValue` | | Short header shown at small widths. | | `headerStyle?` | `Record \| function` | | Inline styles for this column's header cell. | | `headerTooltip?` | `Renderable` | | Info tooltip content shown next to the header. | | `hideAbove?` | `Breakpoint` | | | | `hideBelow?` | `Breakpoint` | | | | `invertSorting?` | `boolean` | | For ranks / "lower is better". | | `lockPin?` | `boolean` | | The user can't unpin/repin via the UI. | | `lockPosition?` | `false \| "first" \| "last"` | | Keeps the column first/last in the order (does not stick while scrolling). | | `maxSize?` | `number` | | | | `maxWidth?` | `string \| number` | | | | `meta?` | `ColumnMeta` | | Free-form metadata (module-augmentable). | | `minSize?` | `number` | ```ts 40 ``` | | | `minWidth?` | `string \| number` | | | | `multiSelectDisplay?` | `"list" \| "autocomplete"` | | | | `noWrap?` | `boolean` | the table's `noWrap` | | | `pin?` | `ResponsiveValue` | | Static (frozen) column side. Responsive: `{ base: 'right', md: false }`. | | `renderFallbackValue?` | `Renderable` | `'—'` (`'-'` in the classic preset) | Shown for `null` / `undefined` / `''`. | | `renderFilter?` | `function` | | Replaces this column's filter control entirely. | | `size?` | `number` | ```ts 150 ``` | Width in px used with resizing / fixed layout. | | `sortDescFirst?` | `boolean \| "auto"` | | | | `sortingFn?` | `SortingFnName \| SortingFn` | | | | `sortServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `sorting[].id`. | | `sortUndefined?` | `false \| 1 \| "first" \| "last" \| -1` | `'last'` | Where null/undefined go regardless of direction. | | `sortValue?` | `function` | | Overrides the value used for sorting. | | `static?` | `boolean` | | Shortcut for `lockPin + enableHiding: false + enableOrdering: false + enableResizing: false + enableColumnActions: false`. | | `truncate?` | `boolean \| object` | | Ellipsis, optionally clamped to N lines, with a tooltip of the full value. | | `type?` | `ColumnType` | | Data type. Drives default align, format, sort and filter variant. | | `verticalAlign?` | `"top" \| "middle" \| "bottom"` | `'middle'` | | | `width?` | `string \| number` | | CSS width; supports `'15%'` or px. | ### AccessorKeyColumnDef (interface) A column reading `row[accessorKey]` (dot paths supported). | Name | Type | Default | Description | | --- | --- | --- | --- | | `accessorFn?` | `undefined` | | | | `accessorKey` | `string & object \| conditional` | | | | `aggregatedCell?` | `ColumnTemplate>` | | | | `aggregationFn?` | `AggregationFnName \| AggregationFn` | | | | `align?` | `ColumnAlign` | `'left'`; `'right'` for `type: 'number'` | | | `cell?` | `ColumnTemplate>` | | Cell content. Defaults to the formatted value. | | `cellClassName?` | `string \| function` | | Class names for this column's body cells. | | `cellStyle?` | `Record \| function` | | Inline styles for this column's body cells. | | `columns?` | `undefined` | | | | `defaultHidden?` | `boolean` | | Initial visibility. | | `enableColumnActions?` | `boolean` | | | | `enableColumnFilter?` | `boolean` | | | | `enableExport?` | `boolean` | ```ts true ``` | | | `enableGlobalFilter?` | `boolean` | `true` for accessor columns | | | `enableGrouping?` | `boolean` | | | | `enableHiding?` | `boolean` | ```ts true ``` | `false` = always visible (not in the columns menu). | | `enableOrdering?` | `boolean` | ```ts true ``` | `false` = can't be reordered. | | `enableResizing?` | `boolean` | ```ts true ``` | | | `enableSorting?` | `boolean` | the table's `enableSorting` | Whether this column can be sorted. | | `exportValue?` | `function` | | The value written to CSV, when it differs from what is displayed. | | `filterFn?` | `FilterFnName \| FilterFn` | | | | `filterOperators?` | `FilterOperator[]` | | | | `filterOptions?` | `FilterOption[] \| function` | | Values offered by the filter control; a function or promise loads them lazily. | | `filterPlaceholder?` | `string` | | | | `filterServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `columnFilters[].id`. | | `filterVariant?` | `FilterVariant` | | | | `footer?` | `ColumnTemplate>` | | Footer content. | | `format?` | `function` | | Display string (also used by search and export). | | `getCellProps?` | `function` | | Extra attributes and events for this column's cells. | | `getGroupingValue?` | `function` | | The value rows are grouped by, when it differs from the cell value. | | `getSearchValue?` | `function` | `format(value)` | What global search matches against. | | `groupedCell?` | `ColumnTemplate>` | | | | `grow?` | `number \| boolean` | | Flex-grow share in grid layout. | | `header?` | `ColumnTemplate>` | | Header content. Defaults to the humanized id. | | `headerAlign?` | `ColumnAlign` | the column's `align` | | | `headerClassName?` | `string \| function` | | Class names for this column's header cell. | | `headerShort?` | `ResponsiveValue` | | Short header shown at small widths. | | `headerStyle?` | `Record \| function` | | Inline styles for this column's header cell. | | `headerTooltip?` | `Renderable` | | Info tooltip content shown next to the header. | | `hideAbove?` | `Breakpoint` | | | | `hideBelow?` | `Breakpoint` | | | | `id?` | `string` | | | | `invertSorting?` | `boolean` | | For ranks / "lower is better". | | `lockPin?` | `boolean` | | The user can't unpin/repin via the UI. | | `lockPosition?` | `false \| "first" \| "last"` | | Keeps the column first/last in the order (does not stick while scrolling). | | `maxSize?` | `number` | | | | `maxWidth?` | `string \| number` | | | | `meta?` | `ColumnMeta` | | Free-form metadata (module-augmentable). | | `minSize?` | `number` | ```ts 40 ``` | | | `minWidth?` | `string \| number` | | | | `multiSelectDisplay?` | `"list" \| "autocomplete"` | | | | `noWrap?` | `boolean` | the table's `noWrap` | | | `pin?` | `ResponsiveValue` | | Static (frozen) column side. Responsive: `{ base: 'right', md: false }`. | | `renderFallbackValue?` | `Renderable` | `'—'` (`'-'` in the classic preset) | Shown for `null` / `undefined` / `''`. | | `renderFilter?` | `function` | | Replaces this column's filter control entirely. | | `size?` | `number` | ```ts 150 ``` | Width in px used with resizing / fixed layout. | | `sortDescFirst?` | `boolean \| "auto"` | | | | `sortingFn?` | `SortingFnName \| SortingFn` | | | | `sortServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `sorting[].id`. | | `sortUndefined?` | `false \| 1 \| "first" \| "last" \| -1` | `'last'` | Where null/undefined go regardless of direction. | | `sortValue?` | `function` | | Overrides the value used for sorting. | | `static?` | `boolean` | | Shortcut for `lockPin + enableHiding: false + enableOrdering: false + enableResizing: false + enableColumnActions: false`. | | `truncate?` | `boolean \| object` | | Ellipsis, optionally clamped to N lines, with a tooltip of the full value. | | `type?` | `ColumnType` | | Data type. Drives default align, format, sort and filter variant. | | `verticalAlign?` | `"top" \| "middle" \| "bottom"` | `'middle'` | | | `width?` | `string \| number` | | CSS width; supports `'15%'` or px. | ### AccessorFnColumnDef (interface) A column computing its value with a function. | Name | Type | Default | Description | | --- | --- | --- | --- | | `accessorFn` | `function` | | Computes this column's value from the row. | | `accessorKey?` | `undefined` | | | | `aggregatedCell?` | `ColumnTemplate>` | | | | `aggregationFn?` | `AggregationFnName \| AggregationFn` | | | | `align?` | `ColumnAlign` | `'left'`; `'right'` for `type: 'number'` | | | `cell?` | `ColumnTemplate>` | | Cell content. Defaults to the formatted value. | | `cellClassName?` | `string \| function` | | Class names for this column's body cells. | | `cellStyle?` | `Record \| function` | | Inline styles for this column's body cells. | | `columns?` | `undefined` | | | | `defaultHidden?` | `boolean` | | Initial visibility. | | `enableColumnActions?` | `boolean` | | | | `enableColumnFilter?` | `boolean` | | | | `enableExport?` | `boolean` | ```ts true ``` | | | `enableGlobalFilter?` | `boolean` | `true` for accessor columns | | | `enableGrouping?` | `boolean` | | | | `enableHiding?` | `boolean` | ```ts true ``` | `false` = always visible (not in the columns menu). | | `enableOrdering?` | `boolean` | ```ts true ``` | `false` = can't be reordered. | | `enableResizing?` | `boolean` | ```ts true ``` | | | `enableSorting?` | `boolean` | the table's `enableSorting` | Whether this column can be sorted. | | `exportValue?` | `function` | | The value written to CSV, when it differs from what is displayed. | | `filterFn?` | `FilterFnName \| FilterFn` | | | | `filterOperators?` | `FilterOperator[]` | | | | `filterOptions?` | `FilterOption[] \| function` | | Values offered by the filter control; a function or promise loads them lazily. | | `filterPlaceholder?` | `string` | | | | `filterServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `columnFilters[].id`. | | `filterVariant?` | `FilterVariant` | | | | `footer?` | `ColumnTemplate>` | | Footer content. | | `format?` | `function` | | Display string (also used by search and export). | | `getCellProps?` | `function` | | Extra attributes and events for this column's cells. | | `getGroupingValue?` | `function` | | The value rows are grouped by, when it differs from the cell value. | | `getSearchValue?` | `function` | `format(value)` | What global search matches against. | | `groupedCell?` | `ColumnTemplate>` | | | | `grow?` | `number \| boolean` | | Flex-grow share in grid layout. | | `header?` | `ColumnTemplate>` | | Header content. Defaults to the humanized id. | | `headerAlign?` | `ColumnAlign` | the column's `align` | | | `headerClassName?` | `string \| function` | | Class names for this column's header cell. | | `headerShort?` | `ResponsiveValue` | | Short header shown at small widths. | | `headerStyle?` | `Record \| function` | | Inline styles for this column's header cell. | | `headerTooltip?` | `Renderable` | | Info tooltip content shown next to the header. | | `hideAbove?` | `Breakpoint` | | | | `hideBelow?` | `Breakpoint` | | | | `id` | `string` | | | | `invertSorting?` | `boolean` | | For ranks / "lower is better". | | `lockPin?` | `boolean` | | The user can't unpin/repin via the UI. | | `lockPosition?` | `false \| "first" \| "last"` | | Keeps the column first/last in the order (does not stick while scrolling). | | `maxSize?` | `number` | | | | `maxWidth?` | `string \| number` | | | | `meta?` | `ColumnMeta` | | Free-form metadata (module-augmentable). | | `minSize?` | `number` | ```ts 40 ``` | | | `minWidth?` | `string \| number` | | | | `multiSelectDisplay?` | `"list" \| "autocomplete"` | | | | `noWrap?` | `boolean` | the table's `noWrap` | | | `pin?` | `ResponsiveValue` | | Static (frozen) column side. Responsive: `{ base: 'right', md: false }`. | | `renderFallbackValue?` | `Renderable` | `'—'` (`'-'` in the classic preset) | Shown for `null` / `undefined` / `''`. | | `renderFilter?` | `function` | | Replaces this column's filter control entirely. | | `size?` | `number` | ```ts 150 ``` | Width in px used with resizing / fixed layout. | | `sortDescFirst?` | `boolean \| "auto"` | | | | `sortingFn?` | `SortingFnName \| SortingFn` | | | | `sortServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `sorting[].id`. | | `sortUndefined?` | `false \| 1 \| "first" \| "last" \| -1` | `'last'` | Where null/undefined go regardless of direction. | | `sortValue?` | `function` | | Overrides the value used for sorting. | | `static?` | `boolean` | | Shortcut for `lockPin + enableHiding: false + enableOrdering: false + enableResizing: false + enableColumnActions: false`. | | `truncate?` | `boolean \| object` | | Ellipsis, optionally clamped to N lines, with a tooltip of the full value. | | `type?` | `ColumnType` | | Data type. Drives default align, format, sort and filter variant. | | `verticalAlign?` | `"top" \| "middle" \| "bottom"` | `'middle'` | | | `width?` | `string \| number` | | CSS width; supports `'15%'` or px. | ### DisplayColumnDef (interface) A column without a value (actions, custom content). | Name | Type | Default | Description | | --- | --- | --- | --- | | `accessorFn?` | `undefined` | | | | `accessorKey?` | `undefined` | | | | `aggregatedCell?` | `ColumnTemplate>` | | | | `aggregationFn?` | `AggregationFnName \| AggregationFn` | | | | `align?` | `ColumnAlign` | `'left'`; `'right'` for `type: 'number'` | | | `cell?` | `ColumnTemplate>` | | Cell content. Defaults to the formatted value. | | `cellClassName?` | `string \| function` | | Class names for this column's body cells. | | `cellStyle?` | `Record \| function` | | Inline styles for this column's body cells. | | `columns?` | `undefined` | | | | `defaultHidden?` | `boolean` | | Initial visibility. | | `enableColumnActions?` | `boolean` | | | | `enableColumnFilter?` | `boolean` | | | | `enableExport?` | `boolean` | ```ts true ``` | | | `enableGlobalFilter?` | `boolean` | `true` for accessor columns | | | `enableGrouping?` | `boolean` | | | | `enableHiding?` | `boolean` | ```ts true ``` | `false` = always visible (not in the columns menu). | | `enableOrdering?` | `boolean` | ```ts true ``` | `false` = can't be reordered. | | `enableResizing?` | `boolean` | ```ts true ``` | | | `enableSorting?` | `boolean` | the table's `enableSorting` | Whether this column can be sorted. | | `exportValue?` | `function` | | The value written to CSV, when it differs from what is displayed. | | `filterFn?` | `FilterFnName \| FilterFn` | | | | `filterOperators?` | `FilterOperator[]` | | | | `filterOptions?` | `FilterOption[] \| function` | | Values offered by the filter control; a function or promise loads them lazily. | | `filterPlaceholder?` | `string` | | | | `filterServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `columnFilters[].id`. | | `filterVariant?` | `FilterVariant` | | | | `footer?` | `ColumnTemplate>` | | Footer content. | | `format?` | `function` | | Display string (also used by search and export). | | `getCellProps?` | `function` | | Extra attributes and events for this column's cells. | | `getGroupingValue?` | `function` | | The value rows are grouped by, when it differs from the cell value. | | `getSearchValue?` | `function` | `format(value)` | What global search matches against. | | `groupedCell?` | `ColumnTemplate>` | | | | `grow?` | `number \| boolean` | | Flex-grow share in grid layout. | | `header?` | `ColumnTemplate>` | | Header content. Defaults to the humanized id. | | `headerAlign?` | `ColumnAlign` | the column's `align` | | | `headerClassName?` | `string \| function` | | Class names for this column's header cell. | | `headerShort?` | `ResponsiveValue` | | Short header shown at small widths. | | `headerStyle?` | `Record \| function` | | Inline styles for this column's header cell. | | `headerTooltip?` | `Renderable` | | Info tooltip content shown next to the header. | | `hideAbove?` | `Breakpoint` | | | | `hideBelow?` | `Breakpoint` | | | | `id` | `string` | | | | `invertSorting?` | `boolean` | | For ranks / "lower is better". | | `lockPin?` | `boolean` | | The user can't unpin/repin via the UI. | | `lockPosition?` | `false \| "first" \| "last"` | | Keeps the column first/last in the order (does not stick while scrolling). | | `maxSize?` | `number` | | | | `maxWidth?` | `string \| number` | | | | `meta?` | `ColumnMeta` | | Free-form metadata (module-augmentable). | | `minSize?` | `number` | ```ts 40 ``` | | | `minWidth?` | `string \| number` | | | | `multiSelectDisplay?` | `"list" \| "autocomplete"` | | | | `noWrap?` | `boolean` | the table's `noWrap` | | | `pin?` | `ResponsiveValue` | | Static (frozen) column side. Responsive: `{ base: 'right', md: false }`. | | `renderFallbackValue?` | `Renderable` | `'—'` (`'-'` in the classic preset) | Shown for `null` / `undefined` / `''`. | | `renderFilter?` | `function` | | Replaces this column's filter control entirely. | | `size?` | `number` | ```ts 150 ``` | Width in px used with resizing / fixed layout. | | `sortDescFirst?` | `boolean \| "auto"` | | | | `sortingFn?` | `SortingFnName \| SortingFn` | | | | `sortServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `sorting[].id`. | | `sortUndefined?` | `false \| 1 \| "first" \| "last" \| -1` | `'last'` | Where null/undefined go regardless of direction. | | `sortValue?` | `function` | | Overrides the value used for sorting. | | `static?` | `boolean` | | Shortcut for `lockPin + enableHiding: false + enableOrdering: false + enableResizing: false + enableColumnActions: false`. | | `truncate?` | `boolean \| object` | | Ellipsis, optionally clamped to N lines, with a tooltip of the full value. | | `type?` | `ColumnType` | | Data type. Drives default align, format, sort and filter variant. | | `verticalAlign?` | `"top" \| "middle" \| "bottom"` | `'middle'` | | | `width?` | `string \| number` | | CSS width; supports `'15%'` or px. | ### GroupColumnDef (interface) A header group spanning several child columns. | Name | Type | Default | Description | | --- | --- | --- | --- | | `accessorFn?` | `undefined` | | | | `accessorKey?` | `undefined` | | | | `aggregatedCell?` | `ColumnTemplate>` | | | | `aggregationFn?` | `AggregationFnName \| AggregationFn` | | | | `align?` | `ColumnAlign` | `'left'`; `'right'` for `type: 'number'` | | | `cell?` | `ColumnTemplate>` | | Cell content. Defaults to the formatted value. | | `cellClassName?` | `string \| function` | | Class names for this column's body cells. | | `cellStyle?` | `Record \| function` | | Inline styles for this column's body cells. | | `columns` | `ColumnDef[]` | | | | `defaultHidden?` | `boolean` | | Initial visibility. | | `enableColumnActions?` | `boolean` | | | | `enableColumnFilter?` | `boolean` | | | | `enableExport?` | `boolean` | ```ts true ``` | | | `enableGlobalFilter?` | `boolean` | `true` for accessor columns | | | `enableGrouping?` | `boolean` | | | | `enableHiding?` | `boolean` | ```ts true ``` | `false` = always visible (not in the columns menu). | | `enableOrdering?` | `boolean` | ```ts true ``` | `false` = can't be reordered. | | `enableResizing?` | `boolean` | ```ts true ``` | | | `enableSorting?` | `boolean` | the table's `enableSorting` | Whether this column can be sorted. | | `exportValue?` | `function` | | The value written to CSV, when it differs from what is displayed. | | `filterFn?` | `FilterFnName \| FilterFn` | | | | `filterOperators?` | `FilterOperator[]` | | | | `filterOptions?` | `FilterOption[] \| function` | | Values offered by the filter control; a function or promise loads them lazily. | | `filterPlaceholder?` | `string` | | | | `filterServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `columnFilters[].id`. | | `filterVariant?` | `FilterVariant` | | | | `footer?` | `ColumnTemplate>` | | Footer content. | | `format?` | `function` | | Display string (also used by search and export). | | `getCellProps?` | `function` | | Extra attributes and events for this column's cells. | | `getGroupingValue?` | `function` | | The value rows are grouped by, when it differs from the cell value. | | `getSearchValue?` | `function` | `format(value)` | What global search matches against. | | `groupedCell?` | `ColumnTemplate>` | | | | `grow?` | `number \| boolean` | | Flex-grow share in grid layout. | | `header?` | `ColumnTemplate>` | | Header content. Defaults to the humanized id. | | `headerAlign?` | `ColumnAlign` | the column's `align` | | | `headerClassName?` | `string \| function` | | Class names for this column's header cell. | | `headerShort?` | `ResponsiveValue` | | Short header shown at small widths. | | `headerStyle?` | `Record \| function` | | Inline styles for this column's header cell. | | `headerTooltip?` | `Renderable` | | Info tooltip content shown next to the header. | | `hideAbove?` | `Breakpoint` | | | | `hideBelow?` | `Breakpoint` | | | | `id?` | `string` | | | | `invertSorting?` | `boolean` | | For ranks / "lower is better". | | `lockPin?` | `boolean` | | The user can't unpin/repin via the UI. | | `lockPosition?` | `false \| "first" \| "last"` | | Keeps the column first/last in the order (does not stick while scrolling). | | `maxSize?` | `number` | | | | `maxWidth?` | `string \| number` | | | | `meta?` | `ColumnMeta` | | Free-form metadata (module-augmentable). | | `minSize?` | `number` | ```ts 40 ``` | | | `minWidth?` | `string \| number` | | | | `multiSelectDisplay?` | `"list" \| "autocomplete"` | | | | `noWrap?` | `boolean` | the table's `noWrap` | | | `pin?` | `ResponsiveValue` | | Static (frozen) column side. Responsive: `{ base: 'right', md: false }`. | | `renderFallbackValue?` | `Renderable` | `'—'` (`'-'` in the classic preset) | Shown for `null` / `undefined` / `''`. | | `renderFilter?` | `function` | | Replaces this column's filter control entirely. | | `size?` | `number` | ```ts 150 ``` | Width in px used with resizing / fixed layout. | | `sortDescFirst?` | `boolean \| "auto"` | | | | `sortingFn?` | `SortingFnName \| SortingFn` | | | | `sortServerKey?` | `string` | ```ts the column id ``` | Name sent to the server in `sorting[].id`. | | `sortUndefined?` | `false \| 1 \| "first" \| "last" \| -1` | `'last'` | Where null/undefined go regardless of direction. | | `sortValue?` | `function` | | Overrides the value used for sorting. | | `static?` | `boolean` | | Shortcut for `lockPin + enableHiding: false + enableOrdering: false + enableResizing: false + enableColumnActions: false`. | | `truncate?` | `boolean \| object` | | Ellipsis, optionally clamped to N lines, with a tooltip of the full value. | | `type?` | `ColumnType` | | Data type. Drives default align, format, sort and filter variant. | | `verticalAlign?` | `"top" \| "middle" \| "bottom"` | `'middle'` | | | `width?` | `string \| number` | | CSS width; supports `'15%'` or px. | ## Related - [Columns](/react-tablekit/columns/) - [Column filters](/react-tablekit/column-filters/) - [Column sizing](/react-tablekit/column-sizing/) --- # TableInstance > The headless instance returned by useDataTable and createTable, with its row, column, cell and header objects. URL: https://react-tablekit.vercel.app/react-tablekit/api/table-instance/ This is the engine. Everything the built-in UI does, it does through these methods, so anything the toolbar can do, your own control can do too. ## Reference ### TableInstance (interface) The table instance. All setters accept an `Updater`. | Name | Type | Default | Description | | --- | --- | --- | --- | | `autosizeAllColumns` | `(measure?: function) => void` | | Sizes every resizable column to its content. | | `autosizeColumn` | `(id: string, measuredWidth?: number) => void` | | Sizes one column to its content; pass a measured width to skip measuring. | | `clearAllFilters` | `() => void` | | Clears the search and every column filter at once. | | `copyToClipboard` | `(opts?: ExportCsvOptions) => Promise` | | Copies the same CSV to the clipboard. | | `exportCsv` | `(opts?: ExportCsvOptions) => Promise` | | Builds CSV text for the requested scope (server exports fetch in chunks). | | `firstPage` | `() => void` | | Jumps to the first page. | | `flushQuery` | `() => void` | | Applies pending debounced query changes immediately. | | `focusCell` | `(rowId: string, columnId: string) => void` | | Moves focus to one cell (no-op without the React layer). | | `getActiveFilterCount` | `() => number` | | How many filters are active, the global search included. | | `getAllColumns` | `() => Column[]` | | The top-level columns, group columns included. | | `getAllFlatColumns` | `() => Column[]` | | Every column, flattened across group levels. | | `getAllLeafColumns` | `() => Column[]` | | Every leaf column (group columns excluded). | | `getBottomRows` | `() => Row[]` | | Rows pinned to the bottom. | | `getBreakpoint` | `() => Breakpoint` | | Resolved breakpoint from the view layer (`'lg'` without one). | | `getCanNextPage` | `() => boolean` | | Whether a next page exists (cursor-aware). | | `getCanPreviousPage` | `() => boolean` | | Whether a previous page exists. | | `getCanSomeRowsExpand` | `() => boolean` | | Whether anything can expand at all (sub-rows, detail panels or lazy children). | | `getCenterHeaderGroups` | `() => HeaderGroup[]` | | Header groups restricted to the unpinned columns. | | `getCenterRows` | `() => Row[]` | | The unpinned rows of the current page. | | `getCenterVisibleLeafColumns` | `() => Column[]` | | The visible leaf columns that are not pinned. | | `getColumn` | `(id: string) => Column \| undefined` | | Looks up a column by id. | | `getCoreRowModel` | `() => RowModel` | | Stage 1: the rows built from `data`, before any filtering. | | `getCursors` | `() => object` | | Cursor pagination: the next/previous cursors from the last result. | | `getDataStatus` | `() => DataStatus` | | Loading, fetching, error and last-updated flags of the data source. | | `getEffectiveColumnPinning` | `() => ColumnPinningState` | | Pinning as actually rendered: state plus locked, static and responsive pins. | | `getExpandedRowModel` | `() => RowModel` | | Stage 5: after expansion (sub-rows of expanded rows included). | | `getFilteredRowModel` | `() => RowModel` | | Stage 2: after the global search and column filters. | | `getFooterGroups` | `() => HeaderGroup[]` | | Footer rows, mirroring the header groups. | | `getGlobalFacetedRowModel` | `() => RowModel` | | The rows the global search sees (column filters applied). | | `getGroupedRowModel` | `() => RowModel` | | Stage 3: after grouping (group rows inserted). | | `getHeaderGroups` | `() => HeaderGroup[]` | | One entry per header row (more than one with grouped columns). | | `getIsAllColumnsVisible` | `() => boolean` | | Whether every column is visible. | | `getIsAllMatchingSelected` | `() => boolean` | | Whether "select all matching" is active with no exclusions. | | `getIsAllPageRowsSelected` | `() => boolean` | | Whether every selectable row on this page is selected. | | `getIsAllRowsExpanded` | `() => boolean` | | Whether every expandable row is expanded. | | `getIsAllRowsSelected` | `() => boolean` | | Whether every selectable row is selected. | | `getIsSomeColumnsPinned` | `(position?: ColumnPinPosition) => boolean` | | Whether anything is pinned (optionally on one side). | | `getIsSomeColumnsVisible` | `() => boolean` | | Whether at least one column is visible. | | `getIsSomePageRowsSelected` | `() => boolean` | | Whether some, but not all, rows on this page are selected. | | `getIsSomeRowsExpanded` | `() => boolean` | | Whether at least one row is expanded. | | `getIsSomeRowsSelected` | `() => boolean` | | Whether some, but not all, rows are selected (indeterminate). | | `getLeafHeaders` | `() => Header[]` | | The leaf headers of the last header row. | | `getLeftHeaderGroups` | `() => HeaderGroup[]` | | Header groups restricted to the left-pinned columns. | | `getLeftVisibleLeafColumns` | `() => Column[]` | | The visible leaf columns pinned to the left. | | `getPageCount` | `() => number` | | Number of pages, derived from the row count and page size. | | `getPageItems` | `(opts?: Omit) => PageItem[]` | | The page buttons and ellipses to render for the current page. | | `getPaginationRowModel` | `() => RowModel` | | Stage 6: the current page. | | `getPrePaginationRowModel` | `() => RowModel` | | The rows that pagination will slice. | | `getQuery` | `() => TableQuery` | | The normalized query, with server keys applied. | | `getRightHeaderGroups` | `() => HeaderGroup[]` | | Header groups restricted to the right-pinned columns. | | `getRightVisibleLeafColumns` | `() => Column[]` | | The visible leaf columns pinned to the right. | | `getRow` | `(id: string, searchAll?: boolean) => Row \| undefined` | | Looks up a row by id; `searchAll` also searches rows outside the current page. | | `getRowChildrenStatus` | `(rowId: string) => object` | | Lazy children state for a row (server `fetchChildren`). | | `getRowCount` | `() => number` | | Total matching rows: the server's count in server mode (`-1` = unknown). | | `getRowModel` | `() => RowModel` | | The rows to render: the end of the pipeline. | | `getSelectedCount` | `() => number` | | How many rows are selected (the server total when selecting all matching). | | `getSelectedRowIds` | `() => string[]` | | The ids of the selected rows. | | `getSelectedRowModel` | `() => RowModel` | | The selected rows that are currently loaded. | | `getSelectionQuery` | `() => SelectionQuery` | | What to send to a server for a bulk action: ids, or the query plus exclusions. | | `getSortedRowModel` | `() => RowModel` | | Stage 4: after sorting. | | `getState` | `() => TableState` | | The current state: internal slices merged with any controlled ones. | | `getTopRows` | `() => Row[]` | | Rows pinned to the top, in display order. | | `getTotalSize` | `() => number` | | Total width in px of the visible leaf columns. | | `getVisibleLeafColumns` | `() => Column[]` | | The leaf columns that are rendered, in display order. | | `initialState` | `TableState` | | | | `insertRow` | `(row: TData, index?: number) => void` | | Optimistically inserts a row at `index` (0 by default). | | `invalidate` | `() => void` | | Marks the data stale so the next mount or focus refetches it. | | `lastPage` | `() => void` | | Jumps to the last page (offset pagination only). | | `moveColumn` | `(id: string, toIndex: number) => void` | | Moves a column to a position among the visible leaf columns. | | `nextPage` | `() => void` | | Advances one page, if there is one. | | `options` | `ResolvedTableOptions` | | | | `previousPage` | `() => void` | | Goes back one page, if there is one. | | `refresh` | `() => Promise` | | Refetches the current query. | | `removeRow` | `(id: string) => void` | | Optimistically removes one loaded row. | | `reset` | `() => void` | | Restores every state slice to `initialState`. | | `resetColumnFilters` | `(defaultState?: boolean) => void` | | Clears the column filters, or restores `initialState` with `true`. | | `resetColumnOrder` | `(defaultState?: boolean) => void` | | Restores the defined order, or `initialState` with `true`. | | `resetColumnPinning` | `(defaultState?: boolean) => void` | | Unpins everything, or restores `initialState` with `true`. | | `resetColumnSizing` | `(defaultState?: boolean) => void` | | Restores the defined widths, or `initialState` with `true`. | | `resetColumnVisibility` | `(defaultState?: boolean) => void` | | Shows every column again, or restores `initialState` with `true`. | | `resetExpanded` | `(defaultState?: boolean) => void` | | Collapses everything, or restores `initialState` with `true`. | | `resetGlobalFilter` | `(defaultState?: boolean) => void` | | Clears the search, or restores `initialState` with `true`. | | `resetGrouping` | `(defaultState?: boolean) => void` | | Removes all grouping, or restores `initialState` with `true`. | | `resetPagination` | `(defaultState?: boolean) => void` | | Returns to page 1, or restores `initialState` with `true`. | | `resetRowPinning` | `(defaultState?: boolean) => void` | | Unpins every row, or restores `initialState` with `true`. | | `resetRowSelection` | `(defaultState?: boolean) => void` | | Clears the selection, or restores `initialState` with `true`. | | `resetSorting` | `(defaultState?: boolean) => void` | | Clears sorting, or restores `initialState` with `true`. | | `scrollToRow` | `(rowId: string, opts?: object) => void` | | Scrolls a row into view (no-op without the React layer). | | `selectAllMatching` | `() => void` | | Selects every row matching the current query, including rows not yet loaded. | | `selectRange` | `(fromRowId: string, toRowId: string, value?: boolean) => void` | | Selects/deselects every selectable row between two rows (inclusive) in display order. | | `setColumnFilters` | `(updater: Updater) => void` | | Replaces every column filter. | | `setColumnOrder` | `(updater: Updater) => void` | | Replaces the display order (ids; unlisted columns keep their defined order). | | `setColumnPinning` | `(updater: Updater) => void` | | Replaces the pinned column ids per side. | | `setColumnSizing` | `(updater: Updater) => void` | | Replaces the per-column widths in px. | | `setColumnSizingInfo` | `(updater: Updater) => void` | | Updates the in-progress resize (drag offsets). | | `setColumnVisibility` | `(updater: Updater) => void` | | Replaces the visibility map (missing ids are visible). | | `setDensity` | `(updater: Updater) => void` | | Sets the row density. | | `setExpanded` | `(updater: Updater) => void` | | Replaces the expansion state (`true` expands every row). | | `setGlobalFilter` | `(updater: Updater) => void` | | Sets the global search term (debounced before it reaches the query). | | `setGrouping` | `(updater: Updater) => void` | | Replaces the grouping columns (multi-level, outermost first). | | `setOptions` | `(updater: Updater>) => void` | | Replaces the options (the React layer calls this on every render). | | `setPageIndex` | `(updater: Updater) => void` | | Goes to a page (0-based); clamped to the available range. | | `setPageSize` | `(updater: Updater) => void` | | Changes the page size, keeping the first visible row where possible. | | `setPagination` | `(updater: Updater) => void` | | Replaces the whole pagination slice. | | `setRowPinning` | `(updater: Updater) => void` | | Replaces the pinned row ids per side. | | `setRowSelection` | `(updater: Updater) => void` | | Replaces the selection (ids, or the `__all` exclusion model in server mode). | | `setSorting` | `(updater: Updater) => void` | | Replaces the sort list. | | `setState` | `(updater: Updater) => void` | | Replaces the whole state. Controlled slices are reported through their `on*Change`. | | `subscribe` | `(listener: function) => function` | | Subscribes to state changes; returns an unsubscribe function. | | `toggleAllColumnsVisible` | `(value?: boolean) => void` | | Shows or hides every hideable column. | | `toggleAllPageRowsSelected` | `(value?: boolean) => void` | | Selects or deselects every selectable row on the current page. | | `toggleAllRowsExpanded` | `(value?: boolean) => void` | | Expands or collapses every expandable row. | | `toggleAllRowsSelected` | `(value?: boolean) => void` | | Selects or deselects every selectable row in the dataset. | | `toggleDensity` | `() => void` | | Cycles compact → standard → comfortable. | | `updateRow` | `(id: string, updater: Updater) => void` | | Optimistically updates one loaded row. | ### Row (interface) A row. Accessor values are cached per row. | Name | Type | Default | Description | | --- | --- | --- | --- | | `_groupingValuesCache` | `Record` | | Aggregated values of a group row, by column id. | | `depth` | `number` | | | | `getAllCells` | `() => Cell[]` | | A cell per defined column, hidden ones included. | | `getCanExpand` | `() => boolean` | | Whether it can expand: it has sub-rows, a detail panel, or lazy children. | | `getCanMultiSelect` | `() => boolean` | | Whether it takes part in multi-selection and range selection. | | `getCanPin` | `() => boolean` | | Whether the user may pin it (`enableRowPinning`). | | `getCanSelect` | `() => boolean` | | Whether this row may be selected (`enableRowSelection`). | | `getCanSelectSubRows` | `() => boolean` | | Whether selecting it cascades to its sub-rows (`enableSubRowSelection`). | | `getCenterVisibleCells` | `() => Cell[]` | | The visible cells of the unpinned columns. | | `getIsAllParentsExpanded` | `() => boolean` | | Whether every ancestor is expanded, i.e. this row is actually visible. | | `getIsAllSubRowsSelected` | `() => boolean` | | Whether every sub-row is selected. | | `getIsDisabled` | `() => boolean` | | Whether the row is disabled: it ignores clicks and cannot be selected. | | `getIsExpanded` | `() => boolean` | | Whether the row is expanded. | | `getIsGrouped` | `() => boolean` | | Whether this is a group row rather than a data row. | | `getIsPinned` | `() => false \| "top" \| "bottom"` | | Whether the row is pinned above or below the scrolling rows. | | `getIsSelected` | `() => boolean` | | Whether this row is selected. | | `getIsSomeSelected` | `() => boolean` | | Whether some, but not all, of its sub-rows are selected (indeterminate). | | `getLeafRows` | `() => Row[]` | | Every descendant leaf row (itself, when it has no children). | | `getLeftVisibleCells` | `() => Cell[]` | | The visible cells of the left-pinned columns. | | `getParentRow` | `() => Row \| undefined` | | The parent row in tree data, or `undefined` at the top level. | | `getParentRows` | `() => Row[]` | | Every ancestor, outermost first. | | `getRightVisibleCells` | `() => Cell[]` | | The visible cells of the right-pinned columns. | | `getUniqueValues` | `(columnId: string) => TValue[]` | | The distinct values of one column across this row and its sub-rows (faceting). | | `getValue` | `(columnId: string) => TValue` | | The accessor value of one column, cached per row. | | `getVisibleCells` | `() => Cell[]` | | The cells that are rendered, in display order. | | `groupingColumnId` | `string \| undefined` | | | | `groupingValue` | `unknown` | | | | `id` | `string` | | | | `index` | `number` | | | | `original` | `TData` | | | | `parentId` | `string \| undefined` | | | | `pin` | `(position: false \| "top" \| "bottom") => void` | | Pins the row to the top or bottom, or unpins it with `false`. | | `renderValue` | `(columnId: string) => TValue \| null` | | Like `getValue`, but `null` instead of `undefined` (for rendering). | | `subRows` | `Row[]` | | | | `toggleExpanded` | `(value?: boolean) => void` | | Expands or collapses the row; toggles when no value is given. | | `toggleSelected` | `(value?: boolean, opts?: object) => void` | | Selects or deselects the row; toggles when no value is given. | ### Column (interface) A resolved column. | Name | Type | Default | Description | | --- | --- | --- | --- | | `accessorFn` | `function \| undefined` | | `undefined` for display and group columns. | | `clearSorting` | `() => void` | | Removes this column from the sort list. | | `columnDef` | `ColumnDef` | | The definition after `columnDefaults` were merged in. | | `columns` | `Column[]` | | Child columns (group columns only). | | `depth` | `number` | | | | `formatValue` | `(value: unknown, row: TData) => string` | | The string used for display/search/export. | | `getAfter` | `(position?: ColumnPinPosition) => number` | | Offset in px from the end of its pinned side. | | `getAggregationFn` | `() => AggregationFn \| undefined` | | The aggregation used for this column's cells in group rows. | | `getAlign` | `() => ColumnAlign` | | Resolved horizontal alignment (numbers default to `'right'`). | | `getAutoSortingFn` | `() => SortingFn` | | The comparator inferred from the column's values. | | `getCanFilter` | `() => boolean` | | Whether the column can be filtered (`enableColumnFilter`). | | `getCanGlobalFilter` | `() => boolean` | | Whether the global search looks at this column (`enableGlobalFilter`). | | `getCanGroup` | `() => boolean` | | Whether the column may be grouped by (`enableGrouping`). | | `getCanHide` | `() => boolean` | | Whether the user may hide it (`enableHiding`). | | `getCanOrder` | `() => boolean` | | Whether the user may reorder it (`enableColumnOrdering`). | | `getCanPin` | `() => boolean` | | Whether the user may pin it (`enableColumnPinning`). | | `getCanResize` | `() => boolean` | | Whether the user may resize it (`enableColumnResizing`). | | `getCanSort` | `() => boolean` | | Whether the user may sort by it (`enableSorting`). | | `getFacetedMinMaxValues` | `() => [number, number] \| undefined` | | Smallest and largest value, for range filters. | | `getFacetedRowModel` | `() => RowModel` | | Rows used to compute this column's facets (every other column's filters applied). | | `getFacetedUniqueValues` | `() => Map` | | Distinct values with their counts, for select and multi-select filters. | | `getFilterFn` | `() => FilterFn \| undefined` | | The filter predicate in use. | | `getFilterIndex` | `() => number` | | Position in the active filter list (`-1` when not filtered). | | `getFilterOperator` | `() => FilterOperator \| undefined` | | The active filter operator, when the variant supports one. | | `getFilterValue` | `() => unknown` | | The active filter value, or `undefined`. | | `getFilterVariant` | `() => FilterVariant` | | The filter control to render (explicit `filterVariant`, or inferred from the type). | | `getFirstSortDir` | `() => "asc" \| "desc"` | | The direction the first click applies (`sortDescFirst`). | | `getFlatColumns` | `() => Column[]` | | This column and every descendant, depth-first. | | `getGroupedIndex` | `() => number` | | Position in the grouping list (`-1` when not grouped). | | `getIndex` | `() => number` | | Position among the visible leaf columns. | | `getIsFiltered` | `() => boolean` | | Whether a filter is active on this column. | | `getIsGrouped` | `() => boolean` | | Whether rows are currently grouped by this column. | | `getIsPinned` | `() => false \| ColumnPinPosition` | | The side this column is pinned to, or `false`. | | `getIsResizing` | `() => boolean` | | Whether a resize drag is in progress on this column. | | `getIsSorted` | `() => false \| "asc" \| "desc"` | | The current sort direction, or `false` when unsorted. | | `getIsVisible` | `() => boolean` | | Whether the column is currently rendered. | | `getLeafColumns` | `() => Column[]` | | The leaf columns under this one (itself, when it has no children). | | `getNextSortingOrder` | `() => false \| "asc" \| "desc"` | | What the next click on the header will do. | | `getPinnedIndex` | `() => number` | | Position within its pinned group. | | `getServerFacets` | `() => FacetResult \| undefined` | | Server facets (from `result.facets` or `fetchFacets`), if loaded. | | `getSize` | `() => number` | | Current width in px (used when resizing is enabled). | | `getSortIndex` | `() => number` | | Position in the multi-sort list (`-1` when unsorted). | | `getSortingFn` | `() => SortingFn` | | The comparator actually used (explicit `sortingFn`, or the inferred one). | | `getStart` | `(position?: ColumnPinPosition) => number` | | Offset in px from the start of its pinned side (sticky positioning). | | `getType` | `() => ColumnType` | | Resolved data type (explicit or auto-detected). | | `id` | `string` | | | | `loadFacets` | `() => Promise` | | Requests this column's facets from the data source (server faceting). | | `parent` | `Column \| undefined` | | | | `pin` | `(position: false \| ColumnPinPosition) => void` | | Pins the column to a side, or unpins it with `false`. | | `resetSize` | `() => void` | | Restores the column's defined width. | | `setFilterValue` | `(updater: unknown, operator?: FilterOperator) => void` | | Sets (or clears, with `undefined`) this column's filter. | | `toggleGrouping` | `() => void` | | Adds or removes this column from the grouping list. | | `toggleSorting` | `(desc?: boolean, multi?: boolean) => void` | | Sorts by this column; `multi` adds it to the sort list instead of replacing it. | | `toggleVisibility` | `(value?: boolean) => void` | | Shows or hides the column; toggles when no value is given. | ### Cell (interface) One cell of a row. | Name | Type | Default | Description | | --- | --- | --- | --- | | `column` | `Column` | | | | `getContext` | `() => CellContext` | | The context object passed to `cell` renderers. | | `getIsAggregated` | `() => boolean` | | Whether this cell shows an aggregate of the group's rows. | | `getIsGrouped` | `() => boolean` | | Whether this is the grouping cell of a group row (it shows the value and the count). | | `getIsPlaceholder` | `() => boolean` | | Whether this cell is blank because another column groups the row. | | `getValue` | `() => TValue` | | The raw accessor value. | | `id` | `string` | | | | `renderValue` | `() => TValue \| null` | | The value for rendering: like `getValue`, but `null` instead of `undefined`. | | `row` | `Row` | | | ### Header (interface) One header cell (multi-row headers supported). | Name | Type | Default | Description | | --- | --- | --- | --- | | `colSpan` | `number` | | | | `column` | `Column` | | | | `depth` | `number` | | | | `getContext` | `() => HeaderContext` | | The context object passed to `header` renderers. | | `getLeafHeaders` | `() => Header[]` | | Every leaf header under this one. | | `getSize` | `() => number` | | Width in px of this header (the sum of its leaf columns for a group header). | | `getStart` | `(position?: ColumnPinPosition) => number` | | Offset in px from the start of its pinned side (sticky positioning). | | `headerGroup` | `HeaderGroup` | | | | `id` | `string` | | | | `index` | `number` | | | | `isPlaceholder` | `boolean` | | Placeholder cells fill the space above leaf columns in multi-row headers. | | `rowSpan` | `number` | | | | `subHeaders` | `Header[]` | | | ## Related - [Headless](/react-tablekit/headless/) - [Hooks](/react-tablekit/api/hooks/) - [Toolbar](/react-tablekit/toolbar/) --- # TableState > Every state slice, the engine options that own them, and the normalized TableQuery sent to a server. URL: https://react-tablekit.vercel.app/react-tablekit/api/table-state/ State is a plain object of independent slices. Pass `initialState` to seed them, `state` to control them, or leave both out and let the table own them. `TableQuery` is the normalized form that travels to a server. ## Reference ### TableState (interface) The complete table state. Every slice can be controlled or uncontrolled. | Name | Type | Default | Description | | --- | --- | --- | --- | | `columnFilters` | `ColumnFiltersState` | | | | `columnOrder` | `ColumnOrderState` | | | | `columnPinning` | `ColumnPinningState` | | | | `columnSizing` | `ColumnSizingState` | | | | `columnSizingInfo` | `ColumnSizingInfoState` | | | | `columnVisibility` | `VisibilityState` | | | | `density` | `Density` | | | | `expanded` | `ExpandedState` | | | | `globalFilter` | `string` | | | | `grouping` | `GroupingState` | | | | `pagination` | `PaginationState` | | | | `rowPinning` | `RowPinningState` | | | | `rowSelection` | `RowSelectionState` | | | | `sorting` | `SortingState` | | | ### TableQuery (interface) The normalized query: everything that affects which rows the server must return. `globalFilter` and text filters are already debounced and min-length gated. | Name | Type | Default | Description | | --- | --- | --- | --- | | `columnFilters` | `ColumnFiltersState` | | | | `expanded?` | `Record` | | | | `globalFilter` | `string` | | | | `grouping` | `GroupingState` | | | | `pagination` | `PaginationState` | | | | `sorting` | `SortingState` | | | ### TableOptions (interface) Engine options. `DataTableProps` adds view-only props on top. | Name | Type | Default | Description | | --- | --- | --- | --- | | `_features?` | `TableFeature[]` | | Advanced: custom features. | | `acknowledgePageLocalFiltering?` | `boolean` | | | | `acknowledgePageLocalSorting?` | `boolean` | | | | `aggregationFns?` | `Record>` | | | | `appendPages?` | `boolean` | | Append each fetched page to the previous ones instead of replacing them. Inferred from a `loadMore` / `infinite` pagination variant; set it explicitly for a custom UI. | | `autoResetExpanded?` | `boolean` | ```ts true ``` | | | `autoResetPageIndex?` | `boolean` | ```ts true in client mode, false with manual pagination ``` | | | `autoResetSelection?` | `boolean` | ```ts false ``` | | | `clearSelectionOnQueryChange?` | `boolean` | ```ts false client / true server ``` | | | `columnDefaults?` | `Partial>` | | Merged into every column. | | `columnResizeDirection?` | `"ltr" \| "rtl"` | `'ltr'` | | | `columnResizeMode?` | `"onChange" \| "onEnd"` | `'onChange'` | | | `columns` | `AnyColumnDef[]` | | | | `data?` | `TData[]` | ```ts [] ``` | Rows (client mode), or the current page (controlled server mode). | | `dataMode?` | `DataMode` | `'client'` (`'server'` when `dataSource` is set) | | | `dataSource?` | `DataSource` | | Server adapter. Implies `dataMode: 'server'`. | | `debugColumns?` | `boolean` | | | | `debugRows?` | `boolean` | | | | `debugTable?` | `boolean` | | | | `dedupeMs?` | `number` | ```ts 0 (in-flight only) ``` | | | `defaultColumn?` | `Partial>` | | Alias of `columnDefaults`. | | `enableColumnActions?` | `boolean` | ```ts false ``` | | | `enableColumnFilters?` | `boolean` | ```ts true ``` | | | `enableColumnOrdering?` | `boolean` | ```ts false ``` | | | `enableColumnPinning?` | `boolean` | ```ts true ``` | | | `enableColumnResizing?` | `boolean` | ```ts false ``` | | | `enableExpanding?` | `boolean` | | Auto when `getSubRows`, `renderDetailPanel` or `getRowCanExpand` is set. | | `enableFacetedValues?` | `boolean` | ```ts true ``` | | | `enableFilters?` | `boolean` | ```ts true ``` | Master switch for all filtering. | | `enableGlobalFilter?` | `boolean` | ```ts true ``` | | | `enableGrouping?` | `boolean` | ```ts false ``` | | | `enableHiding?` | `boolean` | ```ts true ``` | | | `enableMultiRowSelection?` | `boolean \| function` | ```ts true ``` | `false` = single selection. | | `enableMultiSort?` | `boolean` | ```ts true ``` | | | `enablePagination?` | `boolean` | ```ts true ``` | | | `enableRowPinning?` | `boolean \| function` | ```ts false ``` | | | `enableRowSelection?` | `boolean \| function` | ```ts false ``` | | | `enableSorting?` | `boolean` | ```ts true ``` | | | `enableSortingRemoval?` | `boolean` | ```ts true ``` | | | `enableSubRowSelection?` | `boolean \| function` | ```ts true ``` | | | `error?` | `unknown` | | | | `expandMode?` | `"multiple" \| "single"` | `'multiple'` | | | `exportChunkSize?` | `number` | ```ts 1000 ``` | Rows per chunk for server "export all". | | `exportMaxRows?` | `number` | ```ts 10000 ``` | Cap for server "export all". | | `facetingMode?` | `DataMode` | | | | `facets?` | `Record` | | Server facets in controlled server mode (with `manualFaceting`), by column id. | | `fetching?` | `boolean` | | Refetch with existing data (overlay). | | `fetchOnMount?` | `boolean` | ```ts true ``` | | | `filterDebounceMs?` | `number` | ```ts 300 ``` | | | `filterFns?` | `Record>` | | | | `filterFromLeafRows?` | `boolean` | ```ts false ``` | | | `filterMode?` | `DataMode` | | | | `footerAggregationScope?` | `"page" \| "filtered"` | `'filtered'` | | | `getColumnCanGlobalFilter?` | `function` | | Decides per column whether the global search looks at it. | | `getRowCanExpand?` | `function` | | Decides per row whether it can expand (lazy children that are not loaded yet). | | `getRowCanSelect?` | `function` | | Alias of the function form of `enableRowSelection`. | | `getRowId?` | `function` | ```ts index-based ``` | Strongly recommended; used for keys, selection and expansion. | | `getSubRows?` | `function` | | Tree data (client mode). | | `globalFilterFn?` | `FilterFnName \| FilterFn` | `'includesString'` | | | `globalFilterMatch?` | `"all-words" \| "any-word" \| "phrase"` | `'all-words'` | | | `groupedColumnMode?` | `false \| "reorder" \| "remove"` | `'reorder'` | | | `groupingMode?` | `DataMode` | | | | `initialState?` | `InitialTableState` | | | | `isMultiSortEvent?` | `function` | ```ts Shift+click ``` | Decides whether a click adds to the sort list. | | `isRowDisabled?` | `function` | | Disabled rows are dimmed, not selectable/clickable. | | `keepPinnedRows?` | `boolean` | ```ts true ``` | | | `keepPreviousData?` | `boolean` | ```ts true ``` | | | `loading?` | `boolean` | | Initial load (no data yet). | | `locale?` | `string` | `navigator.language` / `'en-US'` | BCP-47 locale for collation/formatting. | | `manualExpanding?` | `boolean` | | | | `manualFaceting?` | `boolean` | | | | `manualFiltering?` | `boolean` | | | | `manualGrouping?` | `boolean` | | | | `manualPagination?` | `boolean` | | | | `manualSorting?` | `boolean` | | | | `maxLeafRowFilterDepth?` | `number` | ```ts Infinity ``` | | | `maxMultiSortColCount?` | `number` | ```ts 3 ``` | | | `mergeOptions?` | `function` | | Advanced: customizes how new options are merged into the instance on each render. | | `meta?` | `TableMeta` | | | | `onColumnFiltersChange?` | `OnChangeFn` | | | | `onColumnOrderChange?` | `OnChangeFn` | | | | `onColumnPinningChange?` | `OnChangeFn` | | | | `onColumnSizingChange?` | `OnChangeFn` | | | | `onColumnSizingInfoChange?` | `OnChangeFn` | | | | `onColumnVisibilityChange?` | `OnChangeFn` | | | | `onDensityChange?` | `OnChangeFn` | | | | `onError?` | `function` | | Called when a data-source request fails, for logging or a toast. | | `onExpandedChange?` | `OnChangeFn` | | | | `onGlobalFilterChange?` | `OnChangeFn` | | | | `onGroupingChange?` | `OnChangeFn` | | | | `onPaginationChange?` | `OnChangeFn` | | | | `onQueryChange?` | `function` | | Called whenever the normalized query changes, with the reason. | | `onRowPinningChange?` | `OnChangeFn` | | | | `onRowSelectionChange?` | `OnChangeFn` | | | | `onSortingChange?` | `OnChangeFn` | | | | `onStateChange?` | `OnChangeFn` | | | | `onStatusChange?` | `function` | | Called whenever the loading/fetching/error status changes. | | `pageCount?` | `number` | | | | `paginateExpandedRows?` | `boolean` | ```ts true ``` | | | `paginationMode?` | `DataMode` | | | | `paginationType?` | `"cursor" \| "offset"` | `'offset'` | | | `refetchInterval?` | `number` | | | | `refetchOnWindowFocus?` | `boolean` | ```ts false ``` | | | `renderFallbackValue?` | `Renderable` | | Table-wide fallback for empty values. | | `resetPageOn?` | `ResetPageReason[]` | ```ts every reason ``` | | | `rowCount?` | `number` | | Total rows in server mode (`-1` = unknown). | | `searchDebounceMs?` | `number` | ```ts 300 ``` | | | `searchMinLength?` | `number` | ```ts 0 ``` | | | `searchMode?` | `DataMode` | | | | `selectAllMode?` | `"all" \| "page"` | `'page'` | | | `sortDescFirst?` | `boolean` | ```ts false ``` | | | `sortingFns?` | `Record>` | | | | `sortingMode?` | `DataMode` | | | | `state?` | `Partial` | | | | `stickyActions?` | `boolean` | | Convenience alias: pins the actions column right. | ### SelectionQuery (variable) ### DataSource (interface) A server adapter. Fetch-library agnostic: return a promise and honour the signal. | Name | Type | Default | Description | | --- | --- | --- | --- | | `fetch` | `(query: TableQuery, ctx: object) => Promise>` | | Called with the normalized query and an AbortSignal. Must resolve the page. | | `fetchAllIds?` | `(query: TableQuery, ctx: object) => Promise` | | Optional: resolve "select all matching" to ids. | | `fetchChildren?` | `(row: Row, query: TableQuery, ctx: object) => Promise` | | Optional: lazy children for tree rows. | | `fetchFacets?` | `(columnId: string, query: TableQuery, ctx: object) => Promise` | | Optional: server-side facets for filter option lists/counts. | ### DataSourceResult (interface) What a data source's `fetch` resolves to. | Name | Type | Default | Description | | --- | --- | --- | --- | | `facets?` | `Record` | | | | `nextCursor?` | `string \| null` | | Cursor pagination. | | `pageCount?` | `number` | | Alternative to `rowCount`. | | `prevCursor?` | `string \| null` | | | | `query?` | `Partial` | | The server may normalize/echo the query; the table adopts it without refetching. | | `rowCount?` | `number` | | Total matching rows (offset pagination). `-1` = unknown. | | `rows` | `TData[]` | | | ### DataSourceState (interface) Live server-mode status returned by useDataSource. | Name | Type | Default | Description | | --- | --- | --- | --- | | `error` | `unknown` | | | | `fetching` | `boolean` | | Refetch with existing data (overlay). | | `lastQuery` | `TableQuery \| null` | | | | `lastUpdated` | `number \| null` | | | | `loading` | `boolean` | | Initial load: no data yet. | | `refresh` | `function` | | Refetches the current query. | | `rowCount` | `number` | | Total matching rows reported by the server (`-1` = unknown). | | `rows` | `TData[]` | | The rows currently loaded from the server. | ## Related - [Client and server](/react-tablekit/client-and-server/) - [Persistence](/react-tablekit/persistence/) - [Hooks](/react-tablekit/api/hooks/) --- # Hooks > Every exported hook: the table itself, server data, state selection, detail panels, breakpoints, virtualization and persistence. URL: https://react-tablekit.vercel.app/react-tablekit/api/hooks/ Each hook is usable on its own. `useDataTable` is the whole engine; the rest are the pieces the built-in component uses internally, exported because a custom layout needs them too. ## Reference ### useDataTable (function) Creates a table instance and subscribes the component to it. Accepts every `` prop; view props are kept on `table.options` for `DataTable.Root`. ```tsx const table = useDataTable({ data, columns, getRowId: (r) => r.id }); return ; ``` `useDataTable(props: DataTableProps): TableInstance`: Creates a table instance and subscribes the component to it. Accepts every `` prop; view props are kept on `table.options` for `DataTable.Root`. ### useDataSource (function) Headless access to server mode: the rows, counts and loading flags of a table driven by `dataSource`. Create the table with the same `dataSource` (`useDataTable({ dataSource })`). `useDataSource(dataSource: DataSource, opts: object): DataSourceState`: Headless access to server mode: the rows, counts and loading flags of a table driven by `dataSource`. Create the table with the same `dataSource` (`useDataTable({ dataSource })`). ### useTableState (function) Subscribes to a slice of table state; the component re-renders only when the selected value changes (fine-grained subscriptions). ```tsx const pagination = useTableState(table, (s) => s.pagination); ``` `useTableState(table: TableInstance, selector: function, isEqual: function): T`: Subscribes to a slice of table state; the component re-renders only when the selected value changes (fine-grained subscriptions). ### useDetailPanelData (function) Loads data for a detail panel, cached per row id and loader. ```tsx const { data, loading } = useDetailPanelData(row, (r) => fetchOrders(r.original.id)); ``` `useDetailPanelData(row: Row, loader: function): object`: Loads data for a detail panel, cached per row id and loader. ### useBreakpoint (function) The current breakpoint, using the theme's breakpoints. `useBreakpoint(): Breakpoint`: The current breakpoint, using the theme's breakpoints. ### useVirtualRows (function) Headless row virtualization: renders only the rows near the viewport of a bounded scroll container, with optional dynamic measurement. ```tsx const { virtualRows, before, after } = useVirtualRows({ count: rows.length, getScrollElement: () => containerRef.current, estimateSize: 44, }); ``` `useVirtualRows(options: UseVirtualRowsOptions): VirtualRowsResult`: Headless row virtualization: renders only the rows near the viewport of a bounded scroll container, with optional dynamic measurement. ### useSyncState (function) Persists table state to the URL and/or storage. On mount the URL wins over storage, which wins over `initialState`. `DataTable` calls this for you when `syncState` is set; call it directly only for a table you built with `createTable`. `useSyncState(table: TableInstance, options: SyncStateOptions | undefined, router?: RouterSyncAdapter): void`: Persists table state to the URL and/or storage. On mount the URL wins over storage, which wins over `initialState`. `DataTable` calls this for you when `syncState` is set; call it directly only for a table you built with `createTable`. ### useRouterSync (function) Plugs `syncState.url` into a router instead of the History API, for React Router, Next.js and friends. ```tsx const [params, setParams] = useSearchParams(); useRouterSync(table, { getSearch: () => params.toString(), setSearch: (search, mode) => setParams(search, { replace: mode === 'replace' }), }); ``` `useRouterSync(table: TableInstance, adapter: RouterSyncAdapter, options?: SyncStateOptions): void`: Plugs `syncState.url` into a router instead of the History API, for React Router, Next.js and friends. ### useTableSlots (function) Inside a custom slot: the resolved slots plus the library defaults, so a replacement can wrap the default. `useTableSlots(): TableSlots & object`: Inside a custom slot: the resolved slots plus the library defaults, so a replacement can wrap the default. ### useTableContext (function) The table instance inside slots and composable parts. `useTableContext(): TableInstance`: The table instance inside slots and composable parts. ## Related - [Headless](/react-tablekit/headless/) - [Persistence](/react-tablekit/persistence/) - [Responsive](/react-tablekit/responsive/) --- # Utilities > The column helper, data-source adapters, CSV export, pagination items, theming helpers and the headless engine factory. URL: https://react-tablekit.vercel.app/react-tablekit/api/utilities/ Standalone functions with no React dependency of their own. They are the seams: the places where you can step outside the component and still use what it uses. ## Reference ### createColumnHelper (function) Typed column builders. ```tsx const col = createColumnHelper(); const columns = [ col.accessor('displayName.companyName', { header: 'Company name' }), col.accessor((r) => r.sites?.length ?? 0, { id: 'siteCount', type: 'number' }), col.display({ id: 'actions', cell: ({ row }) => , pin: 'right', static: true }), ]; ``` `createColumnHelper(): ColumnHelper`: Typed column builders. ### defineColumns (function) Identity with inference, for column arrays defined outside components. `defineColumns(columns: AnyColumnDef[]): AnyColumnDef[]`: Identity with inference, for column arrays defined outside components. ### createRestDataSource (function) A data source for REST endpoints with page/size/sort/search params. ```tsx const customers = createRestDataSource({ url: '/api/customers/search', method: 'POST', mapQuery: (q) => ({ queryCriteria: q.globalFilter, listingCriteria: { pageNumber: q.pagination.pageIndex, pageSize: q.pagination.pageSize } }), mapResult: (json) => ({ rows: json.items, rowCount: json.totalItemCount }), }); ``` `createRestDataSource(options: RestDataSourceOptions): DataSource`: A data source for REST endpoints with page/size/sort/search params. ### createLocalDataSource (function) An in-memory array that *behaves like a server* (latency, failures, abort). It reuses the engine's own filter/sort functions, so client mode and simulated server mode return identical rows for the same query. `createLocalDataSource(rows: TData[] | function, options: LocalDataSourceOptions): DataSource`: An in-memory array that *behaves like a server* (latency, failures, abort). It reuses the engine's own filter/sort functions, so client mode and simulated server mode return identical rows for the same query. ### exportToCsv (function) Standalone CSV export: RFC 4180 quoting, formula-like fields neutralized (`escapeFormulas`), optional UTF-8 BOM, headers from `meta.exportHeader ?? string header ?? id`, values from `exportValue ?? format`. ```tsx const csv = exportToCsv(customers, customerColumns, { bom: false }); ``` `exportToCsv(rows: TData[], columns: AnyColumnDef[], opts: Pick): string`: Standalone CSV export: RFC 4180 quoting, formula-like fields neutralized (`escapeFormulas`), optional UTF-8 BOM, headers from `meta.exportHeader ?? string header ?? id`, values from `exportValue ?? format`. ### getPageItems (function) Pure pagination item generator (0-based). `algorithm` is `'classic'` (default, the corrected classic rule), `'stable'` (constant slot count) or a custom function. ```tsx getPageItems({ pageIndex: 4, pageCount: 10 }); // → pages 0 1 2 3 4 5, ellipsis, pages 8 9 ``` `getPageItems(args: PageItemsArgs): PageItem[]`: Pure pagination item generator (0-based). `algorithm` is `'classic'` (default, the corrected classic rule), `'stable'` (constant slot count) or a custom function. ### createTable (function) Creates a headless table instance. Framework-agnostic: React binds to it through `subscribe` + `getState`. ```tsx const table = createTable({ data, columns, getRowId: (r) => r.id }); table.setSorting([{ id: 'name', desc: false }]); table.getRowModel().rows.map((row) => row.getValue('name')); ``` `createTable(userOptions: TableOptions): TableInstance`: Creates a headless table instance. Framework-agnostic: React binds to it through `subscribe` + `getState`. ### createTheme (function) Deep-merges overrides onto a base theme and returns a complete `TableTheme`. `createTheme(base: TableTheme, overrides: object[]): TableTheme`: Deep-merges overrides onto a base theme and returns a complete `TableTheme`. ### toCssVars (function) Tokens as CSS variables (for inline `style`, SSR or static CSS). `toCssVars(theme: TableThemeTokens): Record`: Tokens as CSS variables (for inline `style`, SSR or static CSS). ### functionalUpdate (function) Resolves an `Updater`: calls it with the old value when it is a function. `functionalUpdate(updater: Updater, old: T): T`: Resolves an `Updater`: calls it with the old value when it is a function. ### sortingFns (variable) ### filterFns (variable) ### aggregationFns (variable) ### flexRender (function) Renders a column template (`header` / `cell` / `footer`) with its context. Functions are called; class components are created; values are rendered as-is. `flexRender(template: ColumnTemplate, props: TProps): ReactNode`: Renders a column template (`header` / `cell` / `footer`) with its context. Functions are called; class components are created; values are rendered as-is. ### ActionButton (function) An icon action button with a tooltip on top. `ActionButton(__namedParameters: ActionButtonProps): Element`: An icon action button with a tooltip on top. ### RowActionsMenu (function) Inline action buttons plus an overflow "⋮" menu. `RowActionsMenu(__namedParameters: RowActionsMenuProps): Element`: Inline action buttons plus an overflow "⋮" menu. ### Tooltip (function) Dark tooltip with portal positioning, shown on hover and focus (no dependency). `Tooltip(props: object): Element`: Dark tooltip with portal positioning, shown on hover and focus (no dependency). ### Checkbox (function) 22px themed checkbox. `Checkbox(props: object): Element`: 22px themed checkbox. ### Chip (function) A small rounded label (optionally clickable / deletable). `Chip(props: Omit, HTMLSpanElement>, "onChange" | "onToggle"> & Record