Data Table
A full-featured data table built on TanStack Table: sticky column pinning, row virtualization for large datasets, dnd-kit drag-to-reorder rows, a paginated footer, a filter toolbar with text / number / range (slider) / date / faceted (select) filters driven by per-column meta, a column-visibility view menu, sortable column headers, and a loading skeleton. Bring your own TanStack table instance; the pieces are composable.
A full-featured data table built on TanStack Table.
You own the table instance (columns, models, state); ikui gives you the
composable UI around it: a scroll container with sticky column pinning, opt-in
row virtualization for large datasets, dnd-kit drag-to-reorder rows, a
paginated footer, and a filter toolbar driven by per-column meta.
The toolbar reads each column's meta.variant and renders the matching control —
text, number, range (a slider popover), date / dateRange (a calendar
popover), and select / multiSelect (a faceted command popover) — alongside a
column-visibility view menu. Sortable headers, a loading skeleton, and a
row drag handle ship as separate pieces you compose as needed.
Installation
One-time setup: add the @ikui registry to your components.json.
{
"registries": {
"@ikui": "https://ik-ui.pages.dev/r/{name}.json"
}
}Then install the component:
pnpm dlx shadcn@latest add @ikui/data-tableUsage
Declare filters through columnDef.meta, then hand the table to DataTable with
DataTableToolbar as its child.
import {
createPaginatedRowModel,
tableFeatures,
useTable,
} from "@tanstack/react-table";
import { DataTable } from "@/components/data-table";
import { DataTableToolbar } from "@/components/data-table-toolbar";
import { DataTableColumnHeader } from "@/components/data-table-column-header";
import { dataTableFeatures } from "@/lib/data-table-utils";
const columns = [
{
accessorKey: "title",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Title" />
),
meta: { label: "Title", variant: "text", placeholder: "Filter titles..." },
enableColumnFilter: true,
filterFn: "includesString",
},
{
accessorKey: "status",
header: "Status",
meta: {
label: "Status",
variant: "select",
options: [
{ label: "Todo", value: "todo" },
{ label: "Done", value: "done" },
],
},
enableColumnFilter: true,
filterFn: "arrIncludesSome",
},
];
function Example() {
const table = useTable({
data,
columns,
features: tableFeatures({
...dataTableFeatures,
paginatedRowModel: createPaginatedRowModel(),
}),
});
return (
<DataTable table={table}>
<DataTableToolbar table={table} />
</DataTable>
);
}Examples
Large datasets (virtualized)
Row virtualization only kicks in when the final row model holds more than 20
rows, so it and pagination are mutually exclusive: omit paginatedRowModel
(or set showPagination={false}) to let the full dataset reach the table, and
give the container a bounded height so it becomes the scroll element the
virtualizer measures against. The example below renders 5,000 rows while only
mounting the handful in view.
Virtualization trims the DOM, not memory or compute — sorting and filtering still run over every row on the client. Past tens of thousands of rows, move to server-side pagination or infinite loading instead.
Row selection
Add a select column that renders a Checkbox bound to the row's selection
state, and a header checkbox for select-all. Pass an actionBar to DataTable
— it renders below the footer whenever rows are selected.
Drag to reorder
Set enableDragAndDrop, give the table a stable getRowId, and add a column
with id "drag" rendering DataTableDragHandle. Reorder your own data in
onDragEnd with dnd-kit's arrayMove.
Column pinning
Pin columns with initialState.columnPinning. Pinned columns stay put with an
inset shadow while the rest of a wide table scrolls horizontally.
Loading skeleton
Render DataTableSkeleton while data is loading. Match columnCount to your
columns; rowCount, filterCount, and the pagination/view toggles are
configurable.
Column meta
The toolbar and filters read these fields off columnDef.meta (types are added
by data-table-utils.ts via module augmentation):
| Prop | Type | Default |
|---|---|---|
label | string | - |
placeholder | string | - |
variant | "text" | "number" | "range" | "date" | "dateRange" | "select" | "multiSelect" | - |
options | { label; value; count?; icon? }[] | - |
range | [number, number] | - |
unit | string | - |
Props
| Prop | Type | Default |
|---|---|---|
table | Table<TData> | - |
enableVirtualization | boolean | true |
estimateRowSize | number | 80 |
enableDragAndDrop | boolean | false |
onDragEnd | (event: DragEndEvent) => void | - |
getRowId | (row: TData) => number | string | - |
showPagination | boolean | true |
totalCount | number | - |
actionBar | React.ReactNode | - |
footer | React.ReactNode | - |
emptyState | React.ReactNode | - |