TanStack Table
Connect Table to TanStack Table for column definitions, sorting, row selection, and pagination. Vortex renders the controls and table; TanStack manages the records and state.
Install TanStack Table
This guide uses TanStack Table v9 in an app with Vortex already set up.
bun add @tanstack/react-table@^9Connect the table
Register sorting, selection, and pagination with tableFeatures. Keep the data and column definitions outside the component so their references stay stable.
| Alex Chen | Designer | |
|---|---|---|
| Mia Rossi | Engineer | |
| Jordan Park | Product manager |
"use client";
import {
type ColumnDef,
createPaginatedRowModel,
createSortedRowModel,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFn_text,
tableFeatures,
useTable,
} from "@tanstack/react-table";
import {
Pagination,
Table,
type TableSortDirection,
Text,
} from "@vortexlabs/vortex";
import type { ReactElement } from "react";
type Member = {
id: string;
name: string;
role: string;
};
const members: Member[] = [
{ id: "alex", name: "Alex Chen", role: "Designer" },
{ id: "mia", name: "Mia Rossi", role: "Engineer" },
{ id: "jordan", name: "Jordan Park", role: "Product manager" },
{ id: "priya", name: "Priya Nair", role: "Engineer" },
{ id: "sam", name: "Sam Lee", role: "Designer" },
{ id: "noah", name: "Noah Kim", role: "Engineer" },
];
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns: { text: sortFn_text },
rowSelectionFeature,
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
});
const columns: ColumnDef<typeof features, Member>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "role", header: "Role" },
];
export const TanStackTablePreview = (): ReactElement => {
const table = useTable({
features,
data: members,
columns,
getRowId: (member: Member): string => member.id,
defaultColumn: { sortFn: "text" },
enableSortingRemoval: false,
initialState: { pagination: { pageIndex: 0, pageSize: 3 } },
});
return (
<Table.Root
ariaLabel="Team members with TanStack Table"
footer={
<>
<Text size="body-sm-desktop" attributes={{ "aria-live": "polite" }}>
{table.getSelectedRowModel().rows.length} of {members.length}{" "}
selected
</Text>
<Pagination
ariaLabel="Member pages"
previousAriaLabel="Previous page"
nextAriaLabel="Next page"
variant="compact"
pageLabel="Page"
totalLabel={({ total }: { total: number }): string => `of ${total}`}
total={table.getPageCount()}
page={table.state.pagination.pageIndex + 1}
onChange={({ page }: { page: number }): void =>
table.setPageIndex(page - 1)
}
/>
</>
}
>
<Table.Header>
{table.getHeaderGroups().map((group) => (
<Table.Row key={group.id}>
<Table.SelectionCell
ariaLabel="Select all members on this page"
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onChange={({ checked }: { checked: boolean }): void =>
table.toggleAllPageRowsSelected(checked)
}
/>
{group.headers.map((header) => (
<Table.HeaderCell
key={header.id}
width={header.column.id === "name" ? "50%" : undefined}
sortDirection={header.column.getIsSorted()}
onSort={
header.column.getCanSort()
? ({
direction,
}: {
direction: TableSortDirection;
}): void =>
header.column.toggleSorting(direction === "desc")
: undefined
}
>
{!header.isPlaceholder && <table.FlexRender header={header} />}
</Table.HeaderCell>
))}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{table.getRowModel().rows.map((row) => (
<Table.Row key={row.id} selected={row.getIsSelected()}>
<Table.SelectionCell
ariaLabel={`Select ${row.original.name}`}
checked={row.getIsSelected()}
onChange={({ checked }: { checked: boolean }): void =>
row.toggleSelected(checked)
}
/>
{row.getAllCells().map((cell) => (
<Table.Cell key={cell.id} rowHeader={cell.column.id === "name"}>
<table.FlexRender cell={cell} />
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};table.FlexRender renders the column definitions inside Vortex header and body cells. This example uses flat columns, with one header row.
Sorting
Pass the column's sort state to Table.HeaderCell. Translate Vortex's next direction into TanStack's descending flag:
<Table.HeaderCell
sortDirection={header.column.getIsSorted()}
onSort={({ direction }) => header.column.toggleSorting(direction === "desc")}
>
<table.FlexRender header={header} />
</Table.HeaderCell>The sorted row model reorders records before pagination. enableSortingRemoval: false keeps the cycle between ascending and descending, matching the Vortex control.
Row selection
Use a stable record ID with getRowId so selection follows a member when sorting or changing pages. Connect each Table.SelectionCell to row.getIsSelected() and row.toggleSelected(checked).
The header checkbox selects only the current page. Its accessible label makes that scope explicit:
<Table.SelectionCell
ariaLabel="Select all members on this page"
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onChange={({ checked }) => table.toggleAllPageRowsSelected(checked)}
/>Selected members remain selected on other pages. The footer counts all selected records in the supplied data.
Pagination
TanStack uses zero-based pageIndex; Pagination starts at one. Convert at the control boundary:
<Pagination
ariaLabel="Member pages"
previousAriaLabel="Previous page"
nextAriaLabel="Next page"
variant="compact"
pageLabel="Page"
totalLabel={({ total }) => `of ${total}`}
total={table.getPageCount()}
page={table.state.pagination.pageIndex + 1}
onChange={({ page }) => table.setPageIndex(page - 1)}
/>This example sorts and paginates the complete supplied dataset in the browser. For server pagination, fetch the requested page and follow TanStack's manual pagination guide.