ZaidanHomeDocsComponentsBlocksChartsTypesetCreate

Command Palette

Search for a command to run...

GitHub
New
Sections
  • Introduction
  • Components
  • Blocks
  • Installation
  • Customization
  • Dark Mode
  • Typeset
  • Zaidan Skills
  • FAQ
  • Roadmap
  • Changelog
Installation
  • Astro
  • Manual
  • Solid Start
  • TanStack Router
  • TanStack Start
  • Vite
Blocks
  • Data Grid
  • Event Calendar
  • Filters
  • Gantt
  • Image Crop
  • Kanban
  • Message Scroller
  • Questionnaire
  • Sortable
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Marker
  • Menubar
  • Message
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip

Data Grid

NameEmailLocationBalance ($)
Sarah Chen
sarah@example.com
gb
United Kingdom
$5243.03
Nick Johnson
nick@example.com
fr
France
$5943.03
Michael Rodriguez
michael@example.com
ca
Canada
$5343.03
Maria Garcia
maria@example.com
jp
Japan
$5843.03
Liam Thompson
liam@example.com
it
Italy
$6043.03
Rows per page
1 - 5 of 10
1
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/solid-table";
2
import { createTable } from "@tanstack/solid-table";
3
import { createSignal } from "solid-js";
4
import {
5
DataGrid,
6
DataGridContainer,
7
type DataGridFeatures,
8
DataGridPagination,
9
DataGridScrollArea,
10
DataGridTable,
11
dataGridFeatures,
12
} from "@/registry/kobalte/blocks/data-grid";
13
14
interface User {
15
id: string;
16
name: string;
17
email: string;
18
flag: string;
19
location: string;
20
balance: number;
21
}
22
23
const demoData: User[] = [
24
{ name: "Alex Johnson", email: "alex@example.com", flag: "us", location: "United States" },
25
{ name: "Sarah Chen", email: "sarah@example.com", flag: "gb", location: "United Kingdom" },
26
{ name: "Michael Rodriguez", email: "michael@example.com", flag: "ca", location: "Canada" },
27
{ name: "Emma Wilson", email: "emma@example.com", flag: "au", location: "Australia" },
28
{ name: "David Kim", email: "david@example.com", flag: "de", location: "Germany" },
29
{ name: "Aron Thompson", email: "lisa@example.com", flag: "my", location: "Malaysia" },
30
{ name: "James Brown", email: "james@example.com", flag: "es", location: "Spain" },
31
{ name: "Maria Garcia", email: "maria@example.com", flag: "jp", location: "Japan" },
32
{ name: "Nick Johnson", email: "nick@example.com", flag: "fr", location: "France" },
33
{ name: "Liam Thompson", email: "liam@example.com", flag: "it", location: "Italy" },
34
].map((user, index) => ({ ...user, id: String(index + 1), balance: 5143.03 + index * 100 }));
35
36
const columns: ColumnDef<DataGridFeatures, User>[] = [
37
{
38
accessorKey: "name",
39
id: "name",
40
header: "Name",
41
cell: (info) => info.getValue<string>(),
42
size: 150,
43
},
44
{
45
accessorKey: "email",
46
id: "email",
47
header: "Email",
48
cell: (info) => (
49
<div class="truncate">
50
<a
51
href={`mailto:${info.getValue<string>()}`}
52
class="truncate hover:text-primary hover:underline"
53
>
54
{info.getValue<string>()}
55
</a>
56
</div>
57
),
58
size: 150,
59
},
60
{
61
accessorKey: "location",
62
id: "location",
63
header: "Location",
64
cell: ({ row }) => (
65
<div class="flex items-center gap-1.5">
66
<img
67
src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
68
alt={row.original.flag}
69
class="size-4 rounded-full object-cover"
70
/>
71
<div class="text-foreground">{row.original.location}</div>
72
</div>
73
),
74
size: 175,
75
},
76
{
77
accessorKey: "balance",
78
id: "balance",
79
header: "Balance ($)",
80
cell: (info) => `$${info.getValue<number>().toFixed(2)}`,
81
size: 100,
82
meta: {
83
headerClassName: "text-right rtl:text-left",
84
cellClassName: "text-right rtl:text-left",
85
},
86
},
87
];
88
89
export default function DataGridDemo() {
90
const [pagination, setPagination] = createSignal<PaginationState>({
91
pageIndex: 0,
92
pageSize: 5,
93
});
94
const [sorting, setSorting] = createSignal<SortingState>([{ id: "name", desc: true }]);
95
96
const table = createTable({
97
features: dataGridFeatures,
98
columns,
99
data: demoData,
100
get pageCount() {
101
return Math.ceil(demoData.length / pagination().pageSize);
102
},
103
getRowId: (row: User) => row.id,
104
state: {
105
get pagination() {
106
return pagination();
107
},
108
get sorting() {
109
return sorting();
110
},
111
},
112
onPaginationChange: setPagination,
113
onSortingChange: setSorting,
114
});
115
116
return (
117
<DataGrid table={table} recordCount={demoData.length}>
118
<div class="w-full space-y-2.5">
119
<DataGridContainer>
120
<DataGridScrollArea>
121
<DataGridTable />
122
</DataGridScrollArea>
123
</DataGridContainer>
124
<DataGridPagination />
125
</div>
126
</DataGrid>
127
);
128
}

The Data Grid block ships the shared grid context, the table renderers, pagination, column controls, drag-and-drop helpers, virtualization, infinite scroll, footer helpers, row pinning, and tree rows. Footer components (DataGridTableFoot, DataGridTableFootRow, DataGridTableFootRowCell) plus the DataGridTableRowPin and DataGridTableRowExpand toggles come from data-grid-table.tsx, and DataGridScrollArea handles scrolling for sticky-header or wide tables.

You own the TanStack core: the block never creates a table for you, it renders the one you hand it.

Installation

pnpm dlx shadcn@latest add @zaidan/data-grid
npx shadcn@latest add @zaidan/data-grid
yarn dlx shadcn@latest add @zaidan/data-grid
bunx --bun shadcn@latest add @zaidan/data-grid

TanStack Table v9

The grid is built on TanStack Table v9 through @tanstack/solid-table. v9 asks every table to declare which features it uses, so the block exports a ready-made bundle:

1
import { createTable } from "@tanstack/solid-table";
2
3
import { dataGridFeatures } from "~/components/blocks/data-grid";
4
5
const table = createTable({
6
features: dataGridFeatures,
7
columns,
8
get data() {
9
return data();
10
},
11
state: {
12
get sorting() {
13
return sorting();
14
},
15
get pagination() {
16
return pagination();
17
},
18
},
19
onSortingChange: setSorting,
20
onPaginationChange: setPagination,
21
});

createTable replaces React's useTable. Its options object is read reactively, so anything that changes over time — data, pageCount, every slice of state — is passed as a getter, not as a snapshot. Solid's setters match TanStack's OnChangeFn shape, so onSortingChange takes a createSignal setter directly.

dataGridFeatures registers everything the grid's own rendering needs, which is a wider set than it looks. columnVisibilityFeature gates row.getVisibleCells() and columnPinningFeature gates the getStartVisibleCells() / getCenterVisibleCells() / getEndVisibleCells() split every row goes through, so a grid that never hides and never pins a column still needs both just to render rows.

Two entries in the bundle are neither features nor row models:

  • sortFns: { basic: sortFn_basic, text: sortFn_text }. On v9 a string sortFn resolves against this registry alone, so "basic" and "text" (plus the default "auto") are the only names this bundle types. Any other built-in ("alphanumeric", "datetime", "textCaseSensitive") has to be registered in your own bundle or passed as a function.
  • columnMeta, which types columnDef.meta as DataGridColumnMeta (headerTitle, headerClassName, cellClassName, skeleton, expandedContent, autoSize). A features-level columnMeta slot wins over the global ColumnMeta interface, so a declare module augmentation is ignored on any table built with dataGridFeatures. Add your own fields to DataGridColumnMeta in the installed data-grid.tsx instead.

The bundle also ships as a type, DataGridFeatures. v9 puts the feature set first in the TanStack generics, so that is the type you write wherever a column, a row, or the table appears in your own annotations:

1
import type { ColumnDef, Row } from "@tanstack/solid-table";
2
3
import type { DataGridFeatures } from "~/components/blocks/data-grid";
4
5
const columns: ColumnDef<DataGridFeatures, User>[] = [
6
// ...
7
];
8
9
function ActionsCell(props: { row: Row<DataGridFeatures, User> }) {
10
// ...
11
}

Extend the bundle when a grid needs more:

1
const features = tableFeatures({
2
...dataGridFeatures,
3
columnGroupingFeature,
4
groupedRowModel: createGroupedRowModel(),
5
});

<DataGrid> is the generic one: its table prop is Table<TFeatures, TData> for any TFeatures, and the instance is widened internally exactly once. The sub-components that take a column, row, or table prop (DataGridColumnHeader, DataGridColumnFilter, DataGridColumnVisibility, DataGridTableRowSelect, DataGridTableRowPin, DataGridTableRowExpand) are declared against DataGridFeatures, and TFeatures is invariant in v9, so any bundle wider or leaner than dataGridFeatures needs a cast at those boundaries.

No Subscribe, no memo

React needs TanStack's Subscribe around builder calls like row.getIsSelected() and column.getIsSorted(), because the React Compiler can memoize a read off a stable row or column reference and freeze it. Solid has no such gap: reading column.getIsSorted() inside JSX is the subscription, since v9 backs its state with signals through the Solid adapter. The block drops Subscribe and memo everywhere upstream used them, and your own cell templates need neither.

State reads go through table.store.state.* — table.getState() is gone, and there is no plain table.state on the Solid adapter:

1
const { pageIndex, pageSize } = table.store.state.pagination;

Upgrading from v8

These are the changes that bite hardest when porting an existing table:

  • Every generic gained a leading TFeatures. ColumnDef<TData, TValue> becomes ColumnDef<DataGridFeatures, TData, TValue>, and Row, Column, Cell, Header, and Table shift the same way.
  • useReactTable is now createTable, and the get*RowModel options are gone. Row models moved into the features bundle, which is what dataGridFeatures hands you.
  • A registered paginatedRowModel always slices. It is the one row model that is not inert, so a grid that used to render every row now shows only pageSize rows (10 by default). When the data is already one page, or the grid renders all rows, set manualPagination: true.
  • Column pinning moved from left / right to start / end, across ColumnPinningState, column.pin(), and getIsPinned(). The grid's data-pinned, data-last-col, and data-outer-pinned-col attributes emit start / end too, so CSS targeting [data-pinned="left"] must be updated.
  • getIsSomeRowsSelected() and getIsSomePageRowsSelected() changed meaning, from "some but not all" to "at least one". Guard an indeterminate checkbox with getIsSomePageRowsSelected() && !getIsAllPageRowsSelected().
  • State shapes got stricter. ColumnPinningState requires both start and end, RowPinningState requires both top and bottom, and RowSelectionState narrowed to Record<string, true>.

Usage

1
import { createSignal } from "solid-js";
2
import { type ColumnDef, createTable } from "@tanstack/solid-table";
3
4
import {
5
DataGrid,
6
DataGridContainer,
7
DataGridPagination,
8
DataGridScrollArea,
9
DataGridTable,
10
DataGridTableFootRow,
11
DataGridTableFootRowCell,
12
type DataGridFeatures,
13
dataGridFeatures,
14
} from "~/components/blocks/data-grid";
1
const columns: ColumnDef<DataGridFeatures, User>[] = [
2
{ accessorKey: "name", id: "name", header: "Name" },
3
{ accessorKey: "email", id: "email", header: "Email" },
4
];
5
6
const footer = (
7
<DataGridTableFootRow>
8
<DataGridTableFootRowCell colSpan={columns.length}>
9
Showing {data().length} rows
10
</DataGridTableFootRowCell>
11
</DataGridTableFootRow>
12
);
13
14
const table = createTable({
15
features: dataGridFeatures,
16
columns,
17
get data() {
18
return data();
19
},
20
});
21
22
return (
23
<DataGrid table={table} recordCount={data().length} tableLayout={{ rowsPinnable: true }}>
24
<DataGridContainer>
25
<DataGridScrollArea>
26
<DataGridTable footerContent={footer} />
27
</DataGridScrollArea>
28
</DataGridContainer>
29
<DataGridPagination />
30
</DataGrid>
31
);

Use DataGridTableRowPin inside a column definition to let users pin rows, swap in DataGridTableVirtual when you need virtualization or infinite scroll, and wrap sticky-header tables with DataGridScrollArea. When rows can be pinned, reordered, or virtualized, provide a stable getRowId so row identity survives reordering.

Examples

Cell Border

NameCompanyOccupationSalary
SC
Sarah Chen
sarah@example.com
OpenAICTO$5243.03
NJ
Nick Johnson
nick@example.com
LVMHData Scientist$5943.03
MR
Michael Rodriguez
michael@example.com
MetaDesigner$5343.03
MG
Maria Garcia
maria@example.com
SonyMarketing Lead$5843.03
LT
Liam Thompson
liam@example.com
ENIEngineer$6043.03
Rows per page
1 - 5 of 10
1
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/solid-table";
2
import { createTable } from "@tanstack/solid-table";
3
import { createSignal } from "solid-js";
4
import {
5
DataGrid,
6
DataGridContainer,
7
type DataGridFeatures,
8
DataGridPagination,
9
DataGridScrollArea,
10
DataGridTable,
11
dataGridFeatures,
12
} from "@/registry/kobalte/blocks/data-grid";
13
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
14
import { Card } from "~/components/ui/card";
15
16
interface User {
17
id: string;
18
name: string;
19
email: string;
20
avatar: string;
21
company: string;
22
role: string;
23
balance: number;
24
}
25
26
const avatars = [
27
"https://github.com/carere.png",
28
"https://github.com/shadcn.png",
29
"https://github.com/evilrabbit.png",
30
"https://github.com/maxleiter.png",
31
];
32
33
const demoData: User[] = [
34
{ name: "Alex Johnson", email: "alex@example.com", company: "Apple", role: "CEO" },
35
{ name: "Sarah Chen", email: "sarah@example.com", company: "OpenAI", role: "CTO" },
36
{ name: "Michael Rodriguez", email: "michael@example.com", company: "Meta", role: "Designer" },
37
{ name: "Emma Wilson", email: "emma@example.com", company: "Tesla", role: "Developer" },
38
{ name: "David Kim", email: "david@example.com", company: "SAP", role: "Lawyer" },
39
{ name: "Aron Thompson", email: "lisa@example.com", company: "Keenthemes", role: "Director" },
40
{ name: "James Brown", email: "james@example.com", company: "BBVA", role: "Product Manager" },
41
{ name: "Maria Garcia", email: "maria@example.com", company: "Sony", role: "Marketing Lead" },
42
{ name: "Nick Johnson", email: "nick@example.com", company: "LVMH", role: "Data Scientist" },
43
{ name: "Liam Thompson", email: "liam@example.com", company: "ENI", role: "Engineer" },
44
].map((user, index) => ({
45
...user,
46
id: String(index + 1),
47
avatar: avatars[index % avatars.length],
48
balance: 5143.03 + index * 100,
49
}));
50
51
function getInitials(name: string) {
52
return name
53
.split(" ")
54
.map((part) => part[0])
55
.join("");
56
}
57
58
const columns: ColumnDef<DataGridFeatures, User>[] = [
59
{
60
accessorKey: "name",
61
id: "name",
62
header: "Name",
63
cell: ({ row }) => (
64
<div class="flex items-center gap-3">
65
<Avatar class="size-8">
66
<AvatarImage src={row.original.avatar} alt={row.original.name} />
67
<AvatarFallback>{getInitials(row.original.name)}</AvatarFallback>
68
</Avatar>
69
<div class="space-y-px">
70
<div class="font-medium text-foreground">{row.original.name}</div>
71
<div class="text-muted-foreground">{row.original.email}</div>
72
</div>
73
</div>
74
),
75
size: 250,
76
enableSorting: true,
77
enableHiding: false,
78
},
79
{
80
accessorKey: "company",
81
id: "company",
82
header: "Company",
83
cell: (info) => <span>{info.getValue<string>()}</span>,
84
size: 100,
85
},
86
{
87
accessorKey: "role",
88
id: "role",
89
header: "Occupation",
90
cell: (info) => <span>{info.getValue<string>()}</span>,
91
size: 100,
92
},
93
{
94
accessorKey: "balance",
95
id: "balance",
96
header: "Salary",
97
cell: (info) => <span class="font-semibold">${info.getValue<number>().toFixed(2)}</span>,
98
size: 100,
99
},
100
];
101
102
export default function DataGridCellBorder() {
103
const [pagination, setPagination] = createSignal<PaginationState>({
104
pageIndex: 0,
105
pageSize: 5,
106
});
107
const [sorting, setSorting] = createSignal<SortingState>([{ id: "name", desc: true }]);
108
109
const table = createTable({
110
features: dataGridFeatures,
111
columns,
112
data: demoData,
113
get pageCount() {
114
return Math.ceil(demoData.length / pagination().pageSize);
115
},
116
getRowId: (row: User) => row.id,
117
state: {
118
get pagination() {
119
return pagination();
120
},
121
get sorting() {
122
return sorting();
123
},
124
},
125
onPaginationChange: setPagination,
126
onSortingChange: setSorting,
127
});
128
129
return (
130
<DataGrid table={table} recordCount={demoData.length} tableLayout={{ cellBorder: true }}>
131
<div class="w-full space-y-2.5">
132
<Card class="p-0">
133
<DataGridContainer>
134
<DataGridScrollArea>
135
<DataGridTable />
136
</DataGridScrollArea>
137
</DataGridContainer>
138
</Card>
139
<DataGridPagination />
140
</div>
141
</DataGrid>
142
);
143
}

Dense Table

NameEmailLocationBalance ($)
SCSarah Chen
sarah@example.com
gb
United Kingdom
$5243.03
NJNick Johnson
nick@example.com
fr
France
$5943.03
MRMichael Rodriguez
michael@example.com
ca
Canada
$5343.03
MGMaria Garcia
maria@example.com
jp
Japan
$5843.03
LTLiam Thompson
liam@example.com
it
Italy
$6043.03
Rows per page
1 - 5 of 10
1
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/solid-table";
2
import { createTable } from "@tanstack/solid-table";
3
import { createSignal } from "solid-js";
4
import {
5
DataGrid,
6
DataGridContainer,
7
type DataGridFeatures,
8
DataGridPagination,
9
DataGridScrollArea,
10
DataGridTable,
11
dataGridFeatures,
12
} from "@/registry/kobalte/blocks/data-grid";
13
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
14
15
interface User {
16
id: string;
17
name: string;
18
email: string;
19
avatar: string;
20
flag: string;
21
location: string;
22
balance: number;
23
}
24
25
const avatars = [
26
"https://github.com/carere.png",
27
"https://github.com/shadcn.png",
28
"https://github.com/evilrabbit.png",
29
"https://github.com/maxleiter.png",
30
];
31
32
const demoData: User[] = [
33
{ name: "Alex Johnson", email: "alex@example.com", flag: "us", location: "United States" },
34
{ name: "Sarah Chen", email: "sarah@example.com", flag: "gb", location: "United Kingdom" },
35
{ name: "Michael Rodriguez", email: "michael@example.com", flag: "ca", location: "Canada" },
36
{ name: "Emma Wilson", email: "emma@example.com", flag: "au", location: "Australia" },
37
{ name: "David Kim", email: "david@example.com", flag: "de", location: "Germany" },
38
{ name: "Aron Thompson", email: "lisa@example.com", flag: "my", location: "Malaysia" },
39
{ name: "James Brown", email: "james@example.com", flag: "es", location: "Spain" },
40
{ name: "Maria Garcia", email: "maria@example.com", flag: "jp", location: "Japan" },
41
{ name: "Nick Johnson", email: "nick@example.com", flag: "fr", location: "France" },
42
{ name: "Liam Thompson", email: "liam@example.com", flag: "it", location: "Italy" },
43
].map((user, index) => ({
44
...user,
45
id: String(index + 1),
46
avatar: avatars[index % avatars.length],
47
balance: 5143.03 + index * 100,
48
}));
49
50
function getInitials(name: string) {
51
return name
52
.split(" ")
53
.map((part) => part[0])
54
.join("");
55
}
56
57
const columns: ColumnDef<DataGridFeatures, User>[] = [
58
{
59
accessorKey: "name",
60
id: "name",
61
header: "Name",
62
cell: ({ row }) => (
63
<div class="flex items-center gap-2">
64
<Avatar class="size-6">
65
<AvatarImage src={row.original.avatar} alt={row.original.name} />
66
<AvatarFallback>{getInitials(row.original.name)}</AvatarFallback>
67
</Avatar>
68
<a href="#name" class="font-medium text-foreground hover:text-primary">
69
{row.original.name}
70
</a>
71
</div>
72
),
73
size: 175,
74
enableSorting: true,
75
enableHiding: false,
76
},
77
{
78
accessorKey: "email",
79
id: "email",
80
header: "Email",
81
cell: (info) => (
82
<a href={`mailto:${info.getValue<string>()}`} class="hover:text-primary hover:underline">
83
{info.getValue<string>()}
84
</a>
85
),
86
size: 150,
87
},
88
{
89
accessorKey: "location",
90
id: "location",
91
header: "Location",
92
cell: ({ row }) => (
93
<div class="flex items-center gap-1.5">
94
<img
95
src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
96
alt={row.original.flag}
97
class="size-4 rounded-full object-cover"
98
/>
99
<div class="font-medium text-foreground">{row.original.location}</div>
100
</div>
101
),
102
size: 150,
103
meta: { cellClassName: "text-start" },
104
},
105
{
106
accessorKey: "balance",
107
id: "balance",
108
header: "Balance ($)",
109
cell: (info) => <span class="font-semibold">${info.getValue<number>().toFixed(2)}</span>,
110
size: 110,
111
meta: {
112
headerClassName: "text-right rtl:text-left",
113
cellClassName: "text-right rtl:text-left",
114
},
115
},
116
];
117
118
export default function DataGridDense() {
119
const [pagination, setPagination] = createSignal<PaginationState>({
120
pageIndex: 0,
121
pageSize: 5,
122
});
123
const [sorting, setSorting] = createSignal<SortingState>([{ id: "name", desc: true }]);
124
125
const table = createTable({
126
features: dataGridFeatures,
127
columns,
128
data: demoData,
129
get pageCount() {
130
return Math.ceil(demoData.length / pagination().pageSize);
131
},
132
getRowId: (row: User) => row.id,
133
state: {
134
get pagination() {
135
return pagination();
136
},
137
get sorting() {
138
return sorting();
139
},
140
},
141
onPaginationChange: setPagination,
142
onSortingChange: setSorting,
143
});
144
145
return (
146
<DataGrid table={table} recordCount={demoData.length} tableLayout={{ dense: true }}>
147
<div class="w-full space-y-2.5">
148
<DataGridContainer>
149
<DataGridScrollArea>
150
<DataGridTable />
151
</DataGridScrollArea>
152
</DataGridContainer>
153
<DataGridPagination />
154
</div>
155
</DataGrid>
156
);
157
}

Light Table

NameLocationStatus
SC
Sarah Chen
sarah@example.com
gb
United Kingdom
Pending
NJ
Nick Johnson
nick@example.com
fr
France
Approved
MR
Michael Rodriguez
michael@example.com
ca
Canada
Approved
MG
Maria Garcia
maria@example.com
jp
Japan
Pending
LT
Liam Thompson
liam@example.com
it
Italy
Pending
Rows per page
1 - 5 of 10
1
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/solid-table";
2
import { createTable } from "@tanstack/solid-table";
3
import { createSignal } from "solid-js";
4
import { cn } from "~/lib/utils";
5
import {
6
DataGrid,
7
DataGridContainer,
8
type DataGridFeatures,
9
DataGridPagination,
10
DataGridScrollArea,
11
DataGridTable,
12
dataGridFeatures,
13
} from "@/registry/kobalte/blocks/data-grid";
14
import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
15
import { Badge } from "~/components/ui/badge";
16
17
type Availability = "online" | "away" | "busy" | "offline";
18
19
interface User {
20
id: string;
21
name: string;
22
email: string;
23
avatar: string;
24
availability: Availability;
25
status: "active" | "inactive";
26
flag: string;
27
location: string;
28
}
29
30
const avatars = [
31
"https://github.com/carere.png",
32
"https://github.com/shadcn.png",
33
"https://github.com/evilrabbit.png",
34
"https://github.com/maxleiter.png",
35
];
36
37
const availabilities: Availability[] = ["online", "away", "busy", "offline"];
38
39
const availabilityColors: Record<Availability, string> = {
40
online: "bg-green-500",
41
away: "bg-yellow-500",
42
busy: "bg-orange-500",
43
offline: "bg-gray-400",
44
};
45
46
const demoData: User[] = [
47
{ name: "Alex Johnson", email: "alex@example.com", flag: "us", location: "United States" },
48
{ name: "Sarah Chen", email: "sarah@example.com", flag: "gb", location: "United Kingdom" },
49
{ name: "Michael Rodriguez", email: "michael@example.com", flag: "ca", location: "Canada" },
50
{ name: "Emma Wilson", email: "emma@example.com", flag: "au", location: "Australia" },
51
{ name: "David Kim", email: "david@example.com", flag: "de", location: "Germany" },
52
{ name: "Aron Thompson", email: "lisa@example.com", flag: "my", location: "Malaysia" },
53
{ name: "James Brown", email: "james@example.com", flag: "es", location: "Spain" },
54
{ name: "Maria Garcia", email: "maria@example.com", flag: "jp", location: "Japan" },
55
{ name: "Nick Johnson", email: "nick@example.com", flag: "fr", location: "France" },
56
{ name: "Liam Thompson", email: "liam@example.com", flag: "it", location: "Italy" },
57
].map((user, index) => ({
58
...user,
59
id: String(index + 1),
60
avatar: avatars[index % avatars.length],
61
availability: availabilities[index % availabilities.length],
62
status: index % 2 === 0 ? ("active" as const) : ("inactive" as const),
63
}));
64
65
function getInitials(name: string) {
66
return name
67
.split(" ")
68
.map((part) => part[0])
69
.join("");
70
}
71
72
const columns: ColumnDef<DataGridFeatures, User>[] = [
73
{
74
accessorKey: "name",
75
id: "name",
76
header: "Name",
77
cell: ({ row }) => (
78
<div class="flex items-center gap-3">
79
<Avatar class="size-8">
80
<AvatarImage src={row.original.avatar} alt={row.original.name} />
81
<AvatarFallback>{getInitials(row.original.name)}</AvatarFallback>
82
<AvatarBadge
83
class={cn(
84
"size-1.5! p-0",
85
availabilityColors[row.original.availability] ?? availabilityColors.offline,
86
)}
87
/>
88
</Avatar>
89
<div class="space-y-px">
90
<div class="font-medium text-foreground">{row.original.name}</div>
91
<div class="text-muted-foreground">{row.original.email}</div>
92
</div>
93
</div>
94
),
95
size: 225,
96
enableSorting: true,
97
enableHiding: false,
98
},
99
{
100
accessorKey: "location",
101
id: "location",
102
header: "Location",
103
cell: ({ row }) => (
104
<div class="flex items-center gap-1.5">
105
<img
106
src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
107
alt={row.original.flag}
108
class="size-4 rounded-full object-cover"
109
/>
110
<div class="font-medium text-foreground">{row.original.location}</div>
111
</div>
112
),
113
size: 160,
114
meta: { cellClassName: "text-start" },
115
},
116
{
117
accessorKey: "status",
118
id: "status",
119
header: "Status",
120
cell: ({ row }) =>
121
row.original.status === "active" ? (
122
<Badge variant="outline" class="text-green-700 dark:text-green-300">
123
Approved
124
</Badge>
125
) : (
126
<Badge variant="outline" class="text-amber-700 dark:text-amber-300">
127
Pending
128
</Badge>
129
),
130
size: 100,
131
},
132
];
133
134
export default function DataGridLight() {
135
const [pagination, setPagination] = createSignal<PaginationState>({
136
pageIndex: 0,
137
pageSize: 5,
138
});
139
const [sorting, setSorting] = createSignal<SortingState>([{ id: "name", desc: true }]);
140
141
const table = createTable({
142
features: dataGridFeatures,
143
columns,
144
data: demoData,
145
get pageCount() {
146
return Math.ceil(demoData.length / pagination().pageSize);
147
},
148
getRowId: (row: User) => row.id,
149
state: {
150
get pagination() {
151
return pagination();
152
},
153
get sorting() {
154
return sorting();
155
},
156
},
157
onPaginationChange: setPagination,
158
onSortingChange: setSorting,
159
});
160
161
return (
162
<DataGrid
163
table={table}
164
recordCount={demoData.length}
165
tableLayout={{ headerBackground: false, rowBorder: false, rowRounded: true }}
166
>
167
<div class="w-full space-y-2.5">
168
<DataGridContainer>
169
<DataGridScrollArea>
170
<DataGridTable />
171
</DataGridScrollArea>
172
</DataGridContainer>
173
<DataGridPagination />
174
</div>
175
</DataGrid>
176
);
177
}

Striped Table

NameEmailLocationBalance ($)
SCSarah Chen
sarah@example.com
gb
United Kingdom
$5243.03
NJNick Johnson
nick@example.com
fr
France
$5943.03
MRMichael Rodriguez
michael@example.com
ca
Canada
$5343.03
MGMaria Garcia
maria@example.com
jp
Japan
$5843.03
LTLiam Thompson
liam@example.com
it
Italy
$6043.03
Rows per page
1 - 5 of 10
1
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/solid-table";
2
import { createTable } from "@tanstack/solid-table";
3
import { createSignal } from "solid-js";
4
import {
5
DataGrid,
6
DataGridContainer,
7
type DataGridFeatures,
8
DataGridPagination,
9
DataGridScrollArea,
10
DataGridTable,
11
dataGridFeatures,
12
} from "@/registry/kobalte/blocks/data-grid";
13
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
14
15
interface User {
16
id: string;
17
name: string;
18
email: string;
19
avatar: string;
20
flag: string;
21
location: string;
22
balance: number;
23
}
24
25
const avatars = [
26
"https://github.com/carere.png",
27
"https://github.com/shadcn.png",
28
"https://github.com/evilrabbit.png",
29
"https://github.com/maxleiter.png",
30
];
31
32
const demoData: User[] = [
33
{ name: "Alex Johnson", email: "alex@example.com", flag: "us", location: "United States" },
34
{ name: "Sarah Chen", email: "sarah@example.com", flag: "gb", location: "United Kingdom" },
35
{ name: "Michael Rodriguez", email: "michael@example.com", flag: "ca", location: "Canada" },
36
{ name: "Emma Wilson", email: "emma@example.com", flag: "au", location: "Australia" },
37
{ name: "David Kim", email: "david@example.com", flag: "de", location: "Germany" },
38
{ name: "Aron Thompson", email: "lisa@example.com", flag: "my", location: "Malaysia" },
39
{ name: "James Brown", email: "james@example.com", flag: "es", location: "Spain" },
40
{ name: "Maria Garcia", email: "maria@example.com", flag: "jp", location: "Japan" },
41
{ name: "Nick Johnson", email: "nick@example.com", flag: "fr", location: "France" },
42
{ name: "Liam Thompson", email: "liam@example.com", flag: "it", location: "Italy" },
43
].map((user, index) => ({
44
...user,
45
id: String(index + 1),
46
avatar: avatars[index % avatars.length],
47
balance: 5143.03 + index * 100,
48
}));
49
50
function getInitials(name: string) {
51
return name
52
.split(" ")
53
.map((part) => part[0])
54
.join("");
55
}
56
57
const columns: ColumnDef<DataGridFeatures, User>[] = [
58
{
59
accessorKey: "name",
60
id: "name",
61
header: "Name",
62
cell: ({ row }) => (
63
<div class="flex items-center gap-2">
64
<Avatar class="size-6">
65
<AvatarImage src={row.original.avatar} alt={row.original.name} />
66
<AvatarFallback>{getInitials(row.original.name)}</AvatarFallback>
67
</Avatar>
68
<a href="#name" class="font-medium text-foreground hover:text-primary">
69
{row.original.name}
70
</a>
71
</div>
72
),
73
size: 160,
74
enableSorting: true,
75
enableHiding: false,
76
},
77
{
78
accessorKey: "email",
79
id: "email",
80
header: "Email",
81
cell: (info) => (
82
<a href={`mailto:${info.getValue<string>()}`} class="hover:text-primary hover:underline">
83
{info.getValue<string>()}
84
</a>
85
),
86
size: 150,
87
},
88
{
89
accessorKey: "location",
90
id: "location",
91
header: "Location",
92
cell: ({ row }) => (
93
<div class="flex items-center gap-1.5">
94
<img
95
src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
96
alt={row.original.flag}
97
class="size-4 rounded-full object-cover"
98
/>
99
<div class="font-medium text-foreground">{row.original.location}</div>
100
</div>
101
),
102
size: 150,
103
},
104
{
105
accessorKey: "balance",
106
id: "balance",
107
header: "Balance ($)",
108
cell: (info) => <span class="font-semibold">${info.getValue<number>().toFixed(2)}</span>,
109
size: 110,
110
meta: {
111
headerClassName: "text-right rtl:text-left",
112
cellClassName: "text-right rtl:text-left",
113
},
114
},
115
];
116
117
export default function DataGridStriped() {
118
const [pagination, setPagination] = createSignal<PaginationState>({
119
pageIndex: 0,
120
pageSize: 5,
121
});
122
const [sorting, setSorting] = createSignal<SortingState>([{ id: "name", desc: true }]);
123
124
const table = createTable({
125
features: dataGridFeatures,
126
columns,
127
data: demoData,
128
get pageCount() {
129
return Math.ceil(demoData.length / pagination().pageSize);
130
},
131
getRowId: (row: User) => row.id,
132
state: {
133
get pagination() {
134
return pagination();
135
},
136
get sorting() {
137
return sorting();
138
},
139
},
140
onPaginationChange: setPagination,
141
onSortingChange: setSorting,
142
});
143
144
return (
145
<DataGrid
146
table={table}
147
recordCount={demoData.length}
148
tableLayout={{ stripped: true, rowRounded: true }}
149
>
150
<div class="w-full space-y-2.5">
151
<DataGridContainer>
152
<DataGridScrollArea>
153
<DataGridTable />
154
</DataGridScrollArea>
155
</DataGridContainer>
156
<DataGridPagination />
157
</div>
158
</DataGrid>
159
);
160
}

Auto Width

NameEmailLocationJoined
SCSarah Chen
sarah@example.com
gb
United Kingdom
Jan, 2024
NJNick Johnson
nick@example.com
fr
France
Jan, 2024
MRMichael Rodriguez
michael@example.com
ca
Canada
Jan, 2024
MGMaria Garcia
maria@example.com
jp
Japan
Jan, 2024
LTLiam Thompson
liam@example.com
it
Italy
Jan, 2024
Rows per page
1 - 5 of 10
1
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/solid-table";
2
import { createTable } from "@tanstack/solid-table";
3
import { createSignal } from "solid-js";
4
import {
5
DataGrid,
6
DataGridContainer,
7
type DataGridFeatures,
8
DataGridPagination,
9
DataGridScrollArea,
10
DataGridTable,
11
dataGridFeatures,
12
} from "@/registry/kobalte/blocks/data-grid";
13
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
14
15
interface User {
16
id: string;
17
name: string;
18
email: string;
19
avatar: string;
20
flag: string;
21
location: string;
22
joined: string;
23
}
24
25
const avatars = [
26
"https://github.com/carere.png",
27
"https://github.com/shadcn.png",
28
"https://github.com/evilrabbit.png",
29
"https://github.com/maxleiter.png",
30
];
31
32
const demoData: User[] = [
33
{ name: "Alex Johnson", email: "alex@example.com", flag: "us", location: "United States" },
34
{ name: "Sarah Chen", email: "sarah@example.com", flag: "gb", location: "United Kingdom" },
35
{ name: "Michael Rodriguez", email: "michael@example.com", flag: "ca", location: "Canada" },
36
{ name: "Emma Wilson", email: "emma@example.com", flag: "au", location: "Australia" },
37
{ name: "David Kim", email: "david@example.com", flag: "de", location: "Germany" },
38
{ name: "Aron Thompson", email: "lisa@example.com", flag: "my", location: "Malaysia" },
39
{ name: "James Brown", email: "james@example.com", flag: "es", location: "Spain" },
40
{ name: "Maria Garcia", email: "maria@example.com", flag: "jp", location: "Japan" },
41
{ name: "Nick Johnson", email: "nick@example.com", flag: "fr", location: "France" },
42
{ name: "Liam Thompson", email: "liam@example.com", flag: "it", location: "Italy" },
43
].map((user, index) => ({
44
...user,
45
id: String(index + 1),
46
avatar: avatars[index % avatars.length],
47
joined: "Jan, 2024",
48
}));
49
50
function getInitials(name: string) {
51
return name
52
.split(" ")
53
.map((part) => part[0])
54
.join("");
55
}
56
57
const columns: ColumnDef<DataGridFeatures, User>[] = [
58
{
59
accessorKey: "name",
60
id: "name",
61
header: "Name",
62
cell: ({ row }) => (
63
<div class="flex items-center gap-2">
64
<Avatar class="size-6">
65
<AvatarImage src={row.original.avatar} alt={row.original.name} />
66
<AvatarFallback>{getInitials(row.original.name)}</AvatarFallback>
67
</Avatar>
68
<a href="#name" class="font-medium text-foreground hover:text-primary">
69
{row.original.name}
70
</a>
71
</div>
72
),
73
size: 225,
74
enableSorting: true,
75
enableHiding: false,
76
},
77
{
78
accessorKey: "email",
79
id: "email",
80
header: "Email",
81
cell: (info) => (
82
<a href={`mailto:${info.getValue<string>()}`} class="hover:text-primary hover:underline">
83
{info.getValue<string>()}
84
</a>
85
),
86
size: 200,
87
},
88
{
89
accessorKey: "location",
90
id: "location",
91
header: "Location",
92
cell: ({ row }) => (
93
<div class="flex items-center gap-1.5">
94
<img
95
src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
96
alt={row.original.flag}
97
class="size-4 rounded-full object-cover"
98
/>
99
<div class="font-medium text-foreground">{row.original.location}</div>
100
</div>
101
),
102
size: 175,
103
},
104
{
105
accessorKey: "joined",
106
id: "joined",
107
header: "Joined",
108
cell: (info) => info.getValue<string>(),
109
size: 120,
110
meta: { cellClassName: "font-medium" },
111
},
112
];
113
114
export default function DataGridAutoWidth() {
115
const [pagination, setPagination] = createSignal<PaginationState>({
116
pageIndex: 0,
117
pageSize: 5,
118
});
119
const [sorting, setSorting] = createSignal<SortingState>([{ id: "name", desc: true }]);
120
121
const table = createTable({
122
features: dataGridFeatures,
123
columns,
124
data: demoData,
125
get pageCount() {
126
return Math.ceil(demoData.length / pagination().pageSize);
127
},
128
getRowId: (row: User) => row.id,
129
state: {
130
get pagination() {
131
return pagination();
132
},
133
get sorting() {
134
return sorting();
135
},
136
},
137
onPaginationChange: setPagination,
138
onSortingChange: setSorting,
139
});
140
141
return (
142
<DataGrid table={table} recordCount={demoData.length} tableLayout={{ width: "auto" }}>
143
<div class="w-full space-y-2.5">
144
<DataGridContainer>
145
<DataGridScrollArea>
146
<DataGridTable />
147
</DataGridScrollArea>
148
</DataGridContainer>
149
<DataGridPagination />
150
</div>
151
</DataGrid>
152
);
153
}

Row Selection

0 of 10 row(s) selected
NameLocationJoined
SC
Sarah Chen
sarah@example.com
gb
United Kingdom
Jan, 2024
NJ
Nick Johnson
nick@example.com
fr
France
Jan, 2024
MR
Michael Rodriguez
michael@example.com
ca
Canada
Jan, 2024
MG
Maria Garcia
maria@example.com
jp
Japan
Jan, 2024
LT
Liam Thompson
liam@example.com
it
Italy
Jan, 2024
Rows per page
1 - 5 of 10
1
import type {
2
ColumnDef,
3
PaginationState,
4
RowSelectionState,
5
SortingState,
6
} from "@tanstack/solid-table";
7
import { createTable } from "@tanstack/solid-table";
8
import { createSignal } from "solid-js";
9
import { cn } from "~/lib/utils";
10
import {
11
DataGrid,
12
DataGridContainer,
13
type DataGridFeatures,
14
DataGridPagination,
15
DataGridScrollArea,
16
DataGridTable,
17
DataGridTableRowSelect,
18
DataGridTableRowSelectAll,
19
dataGridFeatures,
20
} from "@/registry/kobalte/blocks/data-grid";
21
import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
22
23
type Availability = "online" | "away" | "busy" | "offline";
24
25
interface User {
26
id: string;
27
name: string;
28
email: string;
29
avatar: string;
30
availability: Availability;
31
flag: string;
32
location: string;
33
joined: string;
34
}
35
36
const avatars = [
37
"https://github.com/carere.png",
38
"https://github.com/shadcn.png",
39
"https://github.com/evilrabbit.png",
40
"https://github.com/maxleiter.png",
41
];
42
43
const availabilities: Availability[] = ["online", "away", "busy", "offline"];
44
45
const availabilityColors: Record<Availability, string> = {
46
online: "bg-green-500",
47
away: "bg-yellow-500",
48
busy: "bg-orange-500",
49
offline: "bg-gray-400",
50
};
51
52
const demoData: User[] = [
53
{ name: "Alex Johnson", email: "alex@example.com", flag: "us", location: "United States" },
54
{ name: "Sarah Chen", email: "sarah@example.com", flag: "gb", location: "United Kingdom" },
55
{ name: "Michael Rodriguez", email: "michael@example.com", flag: "ca", location: "Canada" },
56
{ name: "Emma Wilson", email: "emma@example.com", flag: "au", location: "Australia" },
57
{ name: "David Kim", email: "david@example.com", flag: "de", location: "Germany" },
58
{ name: "Aron Thompson", email: "lisa@example.com", flag: "my", location: "Malaysia" },
59
{ name: "James Brown", email: "james@example.com", flag: "es", location: "Spain" },
60
{ name: "Maria Garcia", email: "maria@example.com", flag: "jp", location: "Japan" },
61
{ name: "Nick Johnson", email: "nick@example.com", flag: "fr", location: "France" },
62
{ name: "Liam Thompson", email: "liam@example.com", flag: "it", location: "Italy" },
63
].map((user, index) => ({
64
...user,
65
id: String(index + 1),
66
avatar: avatars[index % avatars.length],
67
availability: availabilities[index % availabilities.length],
68
joined: "Jan, 2024",
69
}));
70
71
function getInitials(name: string) {
72
return name
73
.split(" ")
74
.map((part) => part[0])
75
.join("");
76
}
77
78
const columns: ColumnDef<DataGridFeatures, User>[] = [
79
{
80
accessorKey: "id",
81
id: "select",
82
header: () => <DataGridTableRowSelectAll />,
83
cell: ({ row }) => <DataGridTableRowSelect row={row} />,
84
enableSorting: false,
85
size: 20,
86
},
87
{
88
accessorKey: "name",
89
id: "name",
90
header: "Name",
91
cell: ({ row }) => (
92
<div class="flex items-center gap-3">
93
<Avatar class="size-8">
94
<AvatarImage src={row.original.avatar} alt={row.original.name} />
95
<AvatarFallback>{getInitials(row.original.name)}</AvatarFallback>
96
<AvatarBadge
97
class={cn(
98
"size-1.5! p-0",
99
availabilityColors[row.original.availability] ?? availabilityColors.offline,
100
)}
101
/>
102
</Avatar>
103
<div class="space-y-px">
104
<div class="font-medium text-foreground">{row.original.name}</div>
105
<div class="text-muted-foreground">{row.original.email}</div>
106
</div>
107
</div>
108
),
109
size: 200,
110
enableSorting: true,
111
enableHiding: false,
112
},
113
{
114
accessorKey: "location",
115
id: "location",
116
header: "Location",
117
cell: ({ row }) => (
118
<div class="flex items-center gap-1.5">
119
<img
120
src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
121
alt={row.original.flag}
122
class="size-4 rounded-full object-cover"
123
/>
124
<div class="font-medium text-foreground">{row.original.location}</div>
125
</div>
126
),
127
size: 180,
128
meta: { cellClassName: "text-start" },
129
},
130
{
131
accessorKey: "joined",
132
id: "joined",
133
header: "Joined",
134
cell: (info) => info.getValue<string>(),
135
size: 120,
136
meta: { cellClassName: "font-medium" },
137
},
138
];
139
140
export default function DataGridRowSelection() {
141
const [pagination, setPagination] = createSignal<PaginationState>({
142
pageIndex: 0,
143
pageSize: 5,
144
});
145
const [sorting, setSorting] = createSignal<SortingState>([{ id: "name", desc: true }]);
146
const [rowSelection, setRowSelection] = createSignal<RowSelectionState>({});
147
148
const selectedCount = () => Object.keys(rowSelection()).length;
149
150
const table = createTable({
151
features: dataGridFeatures,
152
columns,
153
data: demoData,
154
get pageCount() {
155
return Math.ceil(demoData.length / pagination().pageSize);
156
},
157
getRowId: (row: User) => row.id,
158
state: {
159
get pagination() {
160
return pagination();
161
},
162
get sorting() {
163
return sorting();
164
},
165
get rowSelection() {
166
return rowSelection();
167
},
168
},
169
enableRowSelection: true,
170
onRowSelectionChange: setRowSelection,
171
onPaginationChange: setPagination,
172
onSortingChange: setSorting,
173
});
174
175
return (
176
<DataGrid table={table} recordCount={demoData.length}>
177
<div class="w-full space-y-2.5">
178
<div class="text-muted-foreground text-sm" aria-live="polite">
179
{selectedCount()} of {demoData.length} row(s) selected
180
</div>
181
<DataGridContainer>
182
<DataGridScrollArea>
183
<DataGridTable />
184
</DataGridScrollArea>
185
</DataGridContainer>
186
<DataGridPagination />
187
</div>
188
</DataGrid>
189
);
190
}

Tree Rows

Engineering
Department
-Active
Platform
Team
-Active
AJAlex Johnson
Staff Engineer
us
United States
Active
SCSarah Chen
Senior Engineer
gb
United Kingdom
Active
MRMichael Rodriguez
Frontend Engineer
ca
Canada
Inactive
Mobile
Team
-Active
Design
Department
-Active
Marketing
Department
-Active
Operations
Department
-Active
Rows per page
1 - 4 of 5
1
import type {
2
ColumnDef,
3
ExpandedState,
4
PaginationState,
5
SortingState,
6
} from "@tanstack/solid-table";
7
import { createTable } from "@tanstack/solid-table";
8
import { createSignal, onMount, Show } from "solid-js";
9
import {
10
DataGrid,
11
DataGridColumnHeader,
12
DataGridContainer,
13
type DataGridFeatures,
14
DataGridPagination,
15
DataGridScrollArea,
16
DataGridTable,
17
DataGridTableRowExpand,
18
dataGridFeatures,
19
} from "@/registry/kobalte/blocks/data-grid";
20
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
21
import { Badge } from "~/components/ui/badge";
22
import { Card } from "~/components/ui/card";
23
24
interface Node {
25
id: string;
26
name: string;
27
type: "department" | "team" | "member";
28
status: "active" | "inactive";
29
role?: string;
30
avatar?: string;
31
flag?: string;
32
location?: string;
33
children?: Node[];
34
}
35
36
const demoData: Node[] = [
37
{
38
id: "eng",
39
name: "Engineering",
40
type: "department",
41
status: "active",
42
children: [
43
{
44
id: "eng-platform",
45
name: "Platform",
46
type: "team",
47
status: "active",
48
children: [
49
{
50
id: "eng-platform-1",
51
name: "Alex Johnson",
52
type: "member",
53
status: "active",
54
role: "Staff Engineer",
55
avatar: "https://github.com/shadcn.png",
56
flag: "us",
57
location: "United States",
58
},
59
{
60
id: "eng-platform-2",
61
name: "Sarah Chen",
62
type: "member",
63
status: "active",
64
role: "Senior Engineer",
65
avatar: "https://github.com/evilrabbit.png",
66
flag: "gb",
67
location: "United Kingdom",
68
},
69
{
70
id: "eng-platform-3",
71
name: "Michael Rodriguez",
72
type: "member",
73
status: "inactive",
74
role: "Frontend Engineer",
75
avatar: "https://github.com/maxleiter.png",
76
flag: "ca",
77
location: "Canada",
78
},
79
],
80
},
81
{
82
id: "eng-mobile",
83
name: "Mobile",
84
type: "team",
85
status: "active",
86
children: [
87
{
88
id: "eng-mobile-1",
89
name: "Emma Wilson",
90
type: "member",
91
status: "active",
92
role: "iOS Engineer",
93
avatar: "https://github.com/pranathip.png",
94
flag: "au",
95
location: "Australia",
96
},
97
{
98
id: "eng-mobile-2",
99
name: "David Kim",
100
type: "member",
101
status: "active",
102
role: "Android Engineer",
103
avatar: "https://github.com/shadcn.png",
104
flag: "de",
105
location: "Germany",
106
},
107
],
108
},
109
],
110
},
111
{
112
id: "design",
113
name: "Design",
114
type: "department",
115
status: "active",
116
children: [
117
{
118
id: "design-product",
119
name: "Product Design",
120
type: "team",
121
status: "active",
122
children: [
123
{
124
id: "design-product-1",
125
name: "Aron Thompson",
126
type: "member",
127
status: "active",
128
role: "Design Lead",
129
avatar: "https://github.com/evilrabbit.png",
130
flag: "my",
131
location: "Malaysia",
132
},
133
{
134
id: "design-product-2",
135
name: "Maria Garcia",
136
type: "member",
137
status: "active",
138
role: "Product Designer",
139
avatar: "https://github.com/maxleiter.png",
140
flag: "jp",
141
location: "Japan",
142
},
143
],
144
},
145
{
146
id: "design-brand",
147
name: "Brand",
148
type: "team",
149
status: "active",
150
children: [
151
{
152
id: "design-brand-1",
153
name: "Nick Johnson",
154
type: "member",
155
status: "active",
156
role: "Brand Designer",
157
avatar: "https://github.com/pranathip.png",
158
flag: "fr",
159
location: "France",
160
},
161
{
162
id: "design-brand-2",
163
name: "Liam Thompson",
164
type: "member",
165
status: "inactive",
166
role: "Motion Designer",
167
avatar: "https://github.com/shadcn.png",
168
flag: "it",
169
location: "Italy",
170
},
171
],
172
},
173
],
174
},
175
{
176
id: "marketing",
177
name: "Marketing",
178
type: "department",
179
status: "active",
180
children: [
181
{
182
id: "marketing-growth",
183
name: "Growth",
184
type: "team",
185
status: "active",
186
children: [
187
{
188
id: "marketing-growth-1",
189
name: "Olivia Martin",
190
type: "member",
191
status: "active",
192
role: "Growth Lead",
193
avatar: "https://github.com/evilrabbit.png",
194
flag: "us",
195
location: "United States",
196
},
197
{
198
id: "marketing-growth-2",
199
name: "Ethan Clark",
200
type: "member",
201
status: "active",
202
role: "Performance Marketer",
203
avatar: "https://github.com/maxleiter.png",
204
flag: "ca",
205
location: "Canada",
206
},
207
],
208
},
209
{
210
id: "marketing-content",
211
name: "Content",
212
type: "team",
213
status: "active",
214
children: [
215
{
216
id: "marketing-content-1",
217
name: "Sofia Rossi",
218
type: "member",
219
status: "active",
220
role: "Content Lead",
221
avatar: "https://github.com/pranathip.png",
222
flag: "it",
223
location: "Italy",
224
},
225
{
226
id: "marketing-content-2",
227
name: "Lucas Meyer",
228
type: "member",
229
status: "inactive",
230
role: "Copywriter",
231
avatar: "https://github.com/shadcn.png",
232
flag: "de",
233
location: "Germany",
234
},
235
],
236
},
237
],
238
},
239
{
240
id: "operations",
241
name: "Operations",
242
type: "department",
243
status: "active",
244
children: [
245
{
246
id: "operations-finance",
247
name: "Finance",
248
type: "team",
249
status: "active",
250
children: [
251
{
252
id: "operations-finance-1",
253
name: "Grace Lee",
254
type: "member",
255
status: "active",
256
role: "Finance Lead",
257
avatar: "https://github.com/evilrabbit.png",
258
flag: "kr",
259
location: "South Korea",
260
},
261
{
262
id: "operations-finance-2",
263
name: "Daniel Novak",
264
type: "member",
265
status: "active",
266
role: "Accountant",
267
avatar: "https://github.com/maxleiter.png",
268
flag: "cz",
269
location: "Czechia",
270
},
271
],
272
},
273
{
274
id: "operations-people",
275
name: "People",
276
type: "team",
277
status: "active",
278
children: [
279
{
280
id: "operations-people-1",
281
name: "Chloe Dubois",
282
type: "member",
283
status: "active",
284
role: "People Lead",
285
avatar: "https://github.com/pranathip.png",
286
flag: "fr",
287
location: "France",
288
},
289
{
290
id: "operations-people-2",
291
name: "Ryan Walsh",
292
type: "member",
293
status: "active",
294
role: "Recruiter",
295
avatar: "https://github.com/shadcn.png",
296
flag: "ie",
297
location: "Ireland",
298
},
299
],
300
},
301
],
302
},
303
{
304
id: "sales",
305
name: "Sales",
306
type: "department",
307
status: "active",
308
children: [
309
{
310
id: "sales-accounts",
311
name: "Accounts",
312
type: "team",
313
status: "active",
314
children: [
315
{
316
id: "sales-accounts-1",
317
name: "Mia Park",
318
type: "member",
319
status: "active",
320
role: "Account Executive",
321
avatar: "https://github.com/evilrabbit.png",
322
flag: "kr",
323
location: "South Korea",
324
},
325
{
326
id: "sales-accounts-2",
327
name: "Noah Fischer",
328
type: "member",
329
status: "inactive",
330
role: "Account Manager",
331
avatar: "https://github.com/maxleiter.png",
332
flag: "at",
333
location: "Austria",
334
},
335
],
336
},
337
],
338
},
339
];
340
341
// Collapsed rows are unmounted, so their images would load only when a branch
342
// is first expanded and pop in after the fallback renders. Warming them once
343
// at mount keeps expansion flicker-free.
344
function collectImageUrls(nodes: Node[]): string[] {
345
return nodes.flatMap((node) => [
346
...(node.avatar ? [node.avatar] : []),
347
...(node.flag ? [`https://flagcdn.com/${node.flag.toLowerCase()}.svg`] : []),
348
...(node.children ? collectImageUrls(node.children) : []),
349
]);
350
}
351
352
function getInitials(name: string) {
353
return name
354
.split(" ")
355
.map((part) => part[0])
356
.join("");
357
}
358
359
const columns: ColumnDef<DataGridFeatures, Node>[] = [
360
{
361
accessorKey: "name",
362
id: "name",
363
header: ({ column }) => <DataGridColumnHeader title="Name" column={column} />,
364
cell: ({ row }) => (
365
<div class="flex items-center gap-1">
366
<DataGridTableRowExpand row={row} class="-ms-1.5 -me-1" />
367
<Show
368
when={row.original.type === "member"}
369
fallback={<span class="font-medium text-foreground">{row.original.name}</span>}
370
>
371
<Avatar class="size-6 shrink-0">
372
<AvatarImage src={row.original.avatar} alt={row.original.name} />
373
<AvatarFallback>{getInitials(row.original.name)}</AvatarFallback>
374
</Avatar>
375
<a href="#name" class="font-medium text-foreground hover:text-primary">
376
{row.original.name}
377
</a>
378
</Show>
379
</div>
380
),
381
minSize: 260,
382
enableSorting: true,
383
enableHiding: false,
384
meta: { autoSize: true },
385
},
386
{
387
accessorKey: "role",
388
id: "role",
389
header: ({ column }) => <DataGridColumnHeader title="Role" column={column} />,
390
cell: ({ row }) => (
391
<div class="text-muted-foreground">
392
{row.original.role ?? (row.original.type === "department" ? "Department" : "Team")}
393
</div>
394
),
395
size: 180,
396
},
397
{
398
accessorKey: "location",
399
id: "location",
400
header: ({ column }) => <DataGridColumnHeader title="Location" column={column} />,
401
cell: ({ row }) => (
402
<Show
403
when={row.original.location && row.original.flag}
404
fallback={<span class="text-muted-foreground">-</span>}
405
>
406
<div class="flex items-center gap-1.5">
407
<img
408
src={`https://flagcdn.com/${row.original.flag?.toLowerCase()}.svg`}
409
alt={row.original.flag}
410
class="size-4 rounded-full object-cover"
411
/>
412
<div class="font-medium text-foreground">{row.original.location}</div>
413
</div>
414
</Show>
415
),
416
size: 180,
417
},
418
{
419
accessorKey: "status",
420
id: "status",
421
header: ({ column }) => <DataGridColumnHeader title="Status" column={column} />,
422
cell: ({ row }) =>
423
row.original.status === "active" ? (
424
<Badge variant="outline" class="text-green-700 dark:text-green-300">
425
Active
426
</Badge>
427
) : (
428
<Badge variant="outline" class="text-amber-700 dark:text-amber-300">
429
Inactive
430
</Badge>
431
),
432
size: 130,
433
},
434
];
435
436
export default function DataGridTreeRows() {
437
const [pagination, setPagination] = createSignal<PaginationState>({
438
pageIndex: 0,
439
pageSize: 4,
440
});
441
const [sorting, setSorting] = createSignal<SortingState>([]);
442
const [expanded, setExpanded] = createSignal<ExpandedState>({
443
eng: true,
444
"eng-platform": true,
445
});
446
447
onMount(() => {
448
for (const src of collectImageUrls(demoData)) {
449
const image = new Image();
450
image.src = src;
451
}
452
});
453
454
const table = createTable({
455
features: dataGridFeatures,
456
columns,
457
data: demoData,
458
get pageCount() {
459
return Math.ceil(demoData.length / pagination().pageSize);
460
},
461
getRowId: (row: Node) => row.id,
462
getSubRows: (row: Node) => row.children,
463
state: {
464
get pagination() {
465
return pagination();
466
},
467
get sorting() {
468
return sorting();
469
},
470
get expanded() {
471
return expanded();
472
},
473
},
474
// Keep expanded children on the same page as their parent.
475
paginateExpandedRows: false,
476
onPaginationChange: setPagination,
477
onSortingChange: setSorting,
478
onExpandedChange: setExpanded,
479
});
480
481
return (
482
<DataGrid
483
table={table}
484
recordCount={demoData.length}
485
tableLayout={{ columnsResizable: true, columnsMovable: true, columnsVisibility: true }}
486
>
487
<div class="w-full space-y-2.5">
488
<Card class="overflow-hidden p-0">
489
<DataGridContainer>
490
<DataGridScrollArea>
491
<DataGridTable />
492
</DataGridScrollArea>
493
</DataGridContainer>
494
</Card>
495
<DataGridPagination sizes={[4, 8, 16]} />
496
</div>
497
</DataGrid>
498
);
499
}

Server-Side Rendering

@dnd-kit/solid is browser-only, so DataGridTableDnd and DataGridTableDndRows render the same table markup without a drag context during SSR and hydration, then engage drag and drop one tick after mount. The server HTML is visually identical, but grips are not draggable and carry no drag-related ARIA attributes until hydration completes. Because Solid resolves context through the owner tree, the drag provider has to be an ancestor of the sortables, so the table subtree is built once for the static pass and once when the provider mounts. Every other part of the grid — including DataGridTableVirtual, which measures on the client — renders on the server unchanged.

API Reference

DataGrid

The root component that provides the table context.

PropTypeDefaultDescription
tableTable<TFeatures, TData>-Required. The TanStack Table instance.
recordCountnumber-Required. Total number of records.
isLoadingbooleanfalseWhether the table is in a loading state.
loadingMode"skeleton" | "spinner""skeleton"The visual style of the loading state.
loadingMessageJSX.Element"Loading..."Message displayed when loadingMode is "spinner".
fetchingMoreMessageJSX.ElementloadingMessageMessage displayed while DataGridTableVirtual is fetching more rows.
allRowsLoadedMessageJSX.Element"All records loaded"Message displayed when virtual infinite scroll reaches the end.
emptyMessageJSX.Element"No data available"Message displayed when the table is empty.
onRowClick(row: TData) => void-Callback fired when a row is clicked.
tableLayoutDataGridTableLayout-Configuration for table layout and features.
tableClassNamesDataGridTableClassNames-Custom CSS classes for various table parts.
classstring-Additional CSS classes for the root grid component.

tableLayout

PropertyTypeDefaultDescription
densebooleanfalseWhether to use dense padding for cells.
cellBorderbooleanfalseWhether to show vertical borders between cells.
rowBorderbooleantrueWhether to show horizontal borders between rows.
rowRoundedbooleanfalseWhether to add rounded corners to rows.
strippedbooleanfalseWhether to use zebra-striping for rows.
headerBackgroundbooleanfalseWhether to show a background color for the header.
footerBackgroundbooleanfalseWhether to show a background color for footer rows.
headerBorderbooleantrueWhether to show a border below the header.
headerStickybooleanfalseWhether the header should be sticky during scroll.
width"auto" | "fixed""fixed"The table layout algorithm (table-auto vs table-fixed).
columnsVisibilitybooleanfalseEnables column visibility toggling.
columnsResizablebooleanfalseEnables column resizing.
columnsResizeMode"onChange" | "onEnd""onEnd"When a column resize is committed.
columnsPinnablebooleanfalseEnables column pinning.
columnsMovablebooleanfalseEnables moving columns via the header menu.
columnsDraggablebooleanfalseEnables drag-and-drop for columns.
rowsDraggablebooleanfalseEnables drag-and-drop for rows.
rowsPinnablebooleanfalseEnables row pinning (top/bottom).

columnsResizeMode is resolved by the grid, not by TanStack. Left unset, the grid reads the table's own columnResizeMode and falls back to "onEnd" when that is undefined, which on v9 is the normal case: createTable hands back the options object you passed, so feature defaults never show up on table.options.

DataGridTableClassNames

Custom CSS classes for different parts of the table.

PropertyTypeDefaultDescription
basestring-CSS classes for the <table> element.
headerstring-CSS classes for the <thead> element.
headerRowstring-CSS classes for header rows.
headerStickystring-CSS classes for the sticky header state.
bodystring-CSS classes for the <tbody> element.
bodyRowstring-CSS classes for body rows.
footerstring-CSS classes for the <tfoot> element.
edgeCellstring-CSS classes for the first and last cells in a row.

DataGridContainer

The outer wrapper for the grid. It clips overflow, so scrolling comes from DataGridScrollArea.

PropTypeDefaultDescription
childrenJSX.Element-Required. The grid content to wrap.
borderboolean-Accepted for backwards compatibility and currently has no effect.
classstring-Additional CSS classes for the container.

DataGridScrollArea

Dedicated scroll wrapper for wide grids and sticky headers.

PropTypeDefaultDescription
childrenJSX.Element-Required. The grid content to wrap.
orientation"horizontal" | "vertical" | "both""both"Which scrollbars to render.
classstring-Additional CSS classes for the wrapper.

While the sticky-header scroll mode is active (headerSticky with a vertical orientation), the root carries data-overflow-vertical="true" whenever content overflows vertically. Use it as an ancestor selector to style scrollable vs short grids, for example a closing bottom border on the last row only when a fixed-height grid is partially filled: [[data-slot=data-grid-scroll-area]:not([data-overflow-vertical])_&:last-child>td]:border-b on tableClassNames.bodyRow.

The wrapper is vanilla Solid rather than the Zaidan Scroll Area component: the grid needs a viewport ref, per-axis scrollbars, and a pinned-column inset that the shared wrapper does not expose.

DataGridTable

Renders the actual HTML table. It handles data rendering, loading states (skeletons/spinners), empty states, footer rows, and pinned rows when rowsPinnable is enabled on the parent DataGrid. The unpinned rows come from table.getRowModel(), which on v9 resolves to the paginated row model, and dataGridFeatures always registers paginatedRowModel, so a grid that must render every row sets manualPagination: true.

PropTypeDefaultDescription
footerContentJSX.Element-Optional footer content rendered inside <tfoot>.
renderHeaderbooleantrueWhether to render the table header.

DataGridPagination

Pagination controls. The record info comes from recordCount on DataGrid, and the page buttons come from table.getPageCount(), so they only render when there is more than one page. In v9 getPageCount() is pageCount ?? Math.ceil(rowCount / pageSize) and rowCount falls back to the pre-paginated row count, so a grid whose data holds only the current page must pass rowCount or pageCount to createTable or the buttons never appear.

PropTypeDefaultDescription
sizesnumber[][5, 10, 25, 50, 100]Array of available page sizes.
sizesSkeletonJSX.Element<Skeleton class="h-8 w-44" />Placeholder shown instead of the page size selector while isLoading is set.
rowsPerPageLabelstring"Rows per page"Visible label rendered next to the page size selector.
infostring"{from} - {to} of {count}"Template for the record info. {count} is recordCount.
infoSkeletonJSX.Element<Skeleton class="h-8 w-60" />Placeholder shown instead of the record info while isLoading is set.
moreLimitnumber5The number of page buttons to show before truncating.
previousPageLabelstring"Go to previous page"Accessible label for the previous page button.
nextPageLabelstring"Go to next page"Accessible label for the next page button.
ellipsisTextstring"..."Text displayed for the ellipsis button.
classstring-Additional CSS classes for the pagination container.

sizesInfo, sizesLabel, sizesDescription, and more are still accepted by DataGridPaginationProps but are not rendered. The page size selector is a Kobalte Select, so its value is a number rather than a stringified one, and both skeleton defaults are built lazily — only while isLoading is set.

DataGridColumnHeader

Sort, pin, move, and visibility controls for a column header.

PropTypeDefaultDescription
columnColumn<DataGridFeatures, TData, TValue>-Required. The TanStack Column instance.
titlestring-Header label. Falls back to columnDef.meta.headerTitle, then a string columnDef.header, then column.id.
iconJSX.Element-Optional icon displayed next to the title.
filterJSX.Element-Optional filter component displayed in the header menu.
visibilitybooleanfalseWhether to include column visibility controls in the menu.
classstring-Additional CSS classes for the header label or trigger button.

The props extend Omit<JSX.HTMLAttributes<HTMLDivElement>, "title">, because the string title prop would otherwise collide with the DOM title attribute.

DataGridColumnFilter

A faceted multi-select filter for one column, rendered inside a Popover.

PropTypeDefaultDescription
columnColumn<DataGridFeatures, TData, TValue>-The TanStack Column instance to filter.
titlestring-The title for the filter trigger and placeholder.
optionsArray<{ label: string; value: string; icon?: Component<{ class?: string }> }>-Required. The list of options to filter by.

icon is a Solid component rendered through Dynamic and receives a class prop.

The count beside each option comes from column.getFacetedUniqueValues(). On v9 that method only exists when the feature bundle registers columnFacetingFeature; leave it out of a leaner bundle of your own and the call throws. The numbers themselves come from the facetedUniqueValues row model, and facetedRowModel is what makes those counts respect the table's other active filters. dataGridFeatures registers all three, so the counts work out of the box.

Filtering needs one thing more. v9 resolves a string filterFn name against the filterFns map on the feature bundle, including the default "auto", and dataGridFeatures registers none, so hand the column a filter function directly:

1
import { filterFn_arrHas } from "@tanstack/solid-table";
2
3
const columns = [
4
{
5
accessorKey: "status",
6
id: "status",
7
header: "Status",
8
filterFn: filterFn_arrHas,
9
},
10
];

This filter writes an array of selected values, and filterFn_arrHas matches a scalar cell value against that array (reach for filterFn_arrIncludesSome when the cell itself holds an array).

DataGridColumnVisibility

A dropdown menu of checkbox items, one per hideable column.

PropTypeDefaultDescription
tableTable<DataGridFeatures, TData>-Required. The TanStack Table instance.
triggerComponent<ComponentProps<"button">>-Required. The component rendered as the menu trigger.

Solid has no cloneElement, so trigger is a component rather than an element. It receives the menu trigger props and must spread them onto its root:

1
<DataGridColumnVisibility
2
table={table}
3
trigger={(triggerProps) => (
4
<Button {...triggerProps} variant="outline" size="sm">
5
<Settings2 />
6
Columns
7
</Button>
8
)}
9
/>

DataGridTableDnd

Column drag-and-drop reordering, with optional footer rendering.

PropTypeDefaultDescription
handleDragEnd(event: DataGridTableDndDragEndEvent) => void-Required. Callback fired when a column drag operation ends.
footerContentJSX.Element-Optional footer content rendered inside <tfoot>.

@dnd-kit/solid is the next-generation dnd-kit and has no DragEndEvent, so the grid reports the resolved positions itself:

1
type DataGridTableDndDragEndEvent = {
2
activeId: string;
3
overId: string | null;
4
activeIndex: number;
5
overIndex: number;
6
canceled: boolean;
7
};

The sortable items come from table.store.state.columnOrder, and TanStack starts that slice as an empty array, so you have to seed it and keep it controlled. Give every column an explicit id: ColumnDef.id is optional and is only derived from accessorKey on the built column, not on the definition object you map over.

1
const [columnOrder, setColumnOrder] = createSignal<string[]>(
2
columns.map((column) => column.id as string),
3
);
4
5
const handleDragEnd = (event: DataGridTableDndDragEndEvent) => {
6
if (event.overIndex === -1 || event.activeIndex === event.overIndex) return;
7
setColumnOrder((order) => arrayMove(order, event.activeIndex, event.overIndex));
8
};
9
10
const table = createTable({
11
features: dataGridFeatures,
12
columns,
13
get data() {
14
return data();
15
},
16
state: {
17
get columnOrder() {
18
return columnOrder();
19
},
20
},
21
onColumnOrderChange: setColumnOrder,
22
});

Leave columnOrder empty and the headers never resolve a position, so nothing shifts during the gesture and the reorder commits nothing.

Only the header cell is a sortable. Upstream registers every body cell under the column's id so dnd-kit stamps one translate on the whole column; @dnd-kit/solid keys its registry on unique ids, so the body cells mirror the dragged column's state (dimmed, grabbing cursor, raised) rather than translating with it. The committed order is identical either way.

DataGridTableDndRows

Row drag-and-drop reordering, with optional footer rendering.

Reordering is yours to commit: the grid carries a clone of the row you picked up and marks the seam it would land on, then hands you handleDragEnd to write the new order back. Three things have to line up, and a reorder that silently does nothing is almost always one of them:

  • getRowId must be stable and match dataIds. dnd-kit identifies rows by row.id, so dataIds has to be the same ids in the same order as the rendered rows. A row missing from dataIds resolves to index -1 and will not sort.
  • Reorder by replacing data, not by mutating it. TanStack reprocesses rows when the data reference changes, so an in-place splice leaves the grid showing the old order.
  • Do not read a stale index. Resolve positions from the current data inside the setter.
1
const [rows, setRows] = createSignal(initialRows);
2
const dataIds = () => rows().map((row) => row.id);
3
4
const handleDragEnd = (event: DataGridTableDndRowsDragEndEvent) => {
5
if (event.overIndex === -1 || event.activeIndex === event.overIndex) return;
6
setRows((current) => arrayMove(current, event.activeIndex, event.overIndex));
7
};
8
9
const table = createTable({
10
features: dataGridFeatures,
11
// dataGridFeatures registers a paginated row model, and that model always
12
// slices to pageSize (10 by default). manualPagination says the data is
13
// already the page, so every row renders and stays reorderable.
14
manualPagination: true,
15
columns,
16
get data() {
17
return rows();
18
},
19
getRowId: (row) => row.id,
20
});

The drag handle comes from DataGridTableDndRowHandle in a column of your own. It reads the row's sortable context, so it only works inside the rows this component renders; placed anywhere else it renders as a disabled grip. It takes class, plus a disabled flag and a disabledLabel (default "Reordering unavailable") for the cases where reordering is genuinely off — a sort being the usual one: the grip keeps its place in the gutter and reads as unavailable instead of vanishing and collapsing the column.

PropTypeDefaultDescription
dataIds(string | number)[]-Required. Ids for the current page data, in render order.
handleDragEnd(event: DataGridTableDndRowsDragEndEvent) => void-Required. Callback fired when a row drag operation ends.
footerContentJSX.Element-Optional footer content rendered inside <tfoot>.
collisionDetectionDataGridTableDndRowsCollisionDetectionclosestCenterOverrides the dnd-kit collision strategy.
modifiersDataGridTableDndRowsModifiers-Modifiers forwarded to DragDropProvider untouched.
renderRowDecoration(context) => JSX.Element-Per-row slot for drop indicators and depth guides. Receives { row, isDragging, isOver }.
dropIndicatorbooleantrueMarks the drop target with a bar down its leading edge. Turn off when the decoration paints its own.
onDragStartDragDropProviderProps["onDragStart"]-Forwarded from the drag context, after the internal drag state updates.
onDragMoveDragDropProviderProps["onDragMove"]-Forwarded from the drag context.
onDragOverDragDropProviderProps["onDragOver"]-Forwarded from the drag context.
onDragCancel(event: DataGridTableDndRowsDragEndEvent) => void-Fired with the resolved event when the gesture is aborted.

While a row is in flight the grid does four things, and all of them are built in:

  • The carried row is a clone, measured from the row it was lifted from — its column widths and its height. It is not a fixed size, so a grid with wrapping cells does not appear to grow under the pointer when you pick a row up.
  • The rows hold still. Sliding them apart to open a gap reads well in a list of identical rows and badly in a table. Nothing moves until the drop commits.
  • The row you picked up stays where it was, dimmed and outlined. It is the slot you are moving out of, so it is still there to return to if you change your mind mid-drag.
  • The drop target is marked with a 2px bar down its leading edge. Since nothing moves, the bar is the only thing that says where the row lands, and data-edge says which side of that row it comes to rest on.

The indicator renders as a plain element inside the last cell, never a td of its own, so it adds no column and cannot disturb table-layout: fixed. Target it with [data-slot="data-grid-table-row-drop-indicator"].

Every sortable row carries tree metadata, so any drag event can resolve a target without re-deriving the table shape:

1
type DataGridTableDndRowData = {
2
type: "data-grid-row";
3
depth: number;
4
index: number;
5
parentId: string | null;
6
};

Together these cover cross-parent (re-parenting) drops: track the intended parent and depth in onDragMove or onDragOver, paint the target with renderRowDecoration, and commit the move in handleDragEnd. The decoration node is positioned over the row, so it adds no column and does not disturb striping; give it absolute placement, for example an inset-x-0 bottom-0 h-0.5 bg-primary line offset by the target depth.

@dnd-kit/solid has no SortingStrategy concept, so there is no sortingStrategy prop: the hold-in-place behavior is inherent to how the grid renders. It also re-exports no modifiers, so the default axis restriction and the container clamp are not applied for you; pass your own through modifiers if you need them.

DataGridTableVirtual

A virtualized table renderer using @tanstack/solid-virtual for row virtualization, infinite scroll, optional footer rows, and pinned rows when rowsPinnable is enabled. The wrapper manages row count and the scroll element for you, while virtualizerOptions lets you customize the underlying TanStack Virtual instance. Set scrollToRowIndex to reveal a controlled center row; "auto" alignment keeps already-visible rows in place and accounts for sticky headers.

PropTypeDefaultDescription
heightnumber | string-Optional fixed height when not using an outer scroll container. Numbers are treated as px.
estimateSizenumber48Estimated row height in pixels for the virtualizer.
overscannumber10Number of rows rendered outside the visible area.
scrollBehaviorScrollBehavior"auto"Scroll animation used when revealing scrollToRowIndex.
scrollToRowAlign"auto" | "center" | "start" | "end""auto"Alignment used when revealing the target row. "auto" only scrolls when the row is out of view.
scrollToRowIndexnumber-Index within the center (non-pinned) row section to reveal.
footerContentJSX.Element-Optional footer content rendered inside <tfoot>.
renderHeaderbooleantrueWhether to render the table header.
onFetchMore() => void-Callback fired when the user scrolls near the bottom.
isFetchingMoreboolean-Whether additional data is currently being loaded.
hasMoreboolean-Whether there are more records available to fetch.
fetchMoreOffsetnumber0How many rows before the end should trigger onFetchMore.
virtualizerOptionsDataGridTableVirtualizerOptions<TData>-Passthrough for TanStack Virtual settings like enabled, getItemKey, measureElement, onChange.

observeElementRect, observeElementOffset, and scrollToFn are optional on DataGridTableVirtualizerOptions, matching the Solid adapter's own signature, so passing virtualizerOptions never forces you to supply internals you did not mean to touch.

Set manualPagination: true. A virtualized grid renders every row, and dataGridFeatures registers paginatedRowModel, which is the one row model in the bundle that is not inert: table.getRowModel() is sliced to pageSize (default 10) unless the table opts out. Without it an otherwise correct virtual grid renders ten rows and stops.

DataGridTableRowPin

A pin/unpin toggle button for use in column definitions.

PropTypeDefaultDescription
rowRow<DataGridFeatures, TData>-Required. The TanStack Table row instance.

The button pins to the top region with row.pin("top") and clears it with row.pin(false). Turn pinning on with enableRowPinning on the table and tableLayout={{ rowsPinnable: true }} on DataGrid. In v9 both RowPinningState keys are required, so controlled state has to seed each region: createSignal<RowPinningState>({ top: [], bottom: [] }).

DataGridTableRowSelect / DataGridTableRowSelectAll

Selection checkboxes for a row and for the current page. DataGridTableRowSelect takes a row prop; DataGridTableRowSelectAll reads the table from the grid context and takes none. Enable selection with enableRowSelection on the table.

DataGridTableRowExpand

A depth-indented expand/collapse toggle for tree data, for use in the tree column's cell. Renders a chevron button for expandable rows (with aria-expanded reflecting state) and a compact spacer for leaves so leaf content sits close to the parent label.

PropTypeDefaultDescription
rowRow<DataGridFeatures, TData>-Required. The TanStack Table row instance.
indentnumber20Horizontal offset in px applied per tree depth level.
classstring-Additional CSS classes for the wrapper.
childrenJSX.Element-Custom toggle icon; replaces the default chevron.

Pass children to swap the default chevron for your own state-aware icon; the button always carries aria-expanded, so pure-CSS state styling keeps working. The wrapper exposes data-slot="data-grid-table-row-expand" and the computed --data-grid-tree-padding CSS variable as styling hooks. For fully custom cells, the exported getDataGridTreeIndentStyle(row, indent) helper returns the same indent style, and row.getCanExpand() / row.getIsExpanded() / row.getToggleExpandedHandler() cover bring-your-own toggles.

DataGridTableFoot

Wrapper component for the table footer (<tfoot>).

DataGridTableFootRow

A row inside the table footer.

DataGridTableFootRowCell

A cell inside a footer row.

PropTypeDefaultDescription
colSpannumber-Column span for the footer cell.
classstring-Additional CSS classes.
childrenJSX.Element-Content of the footer cell.

DOM Attributes

Data rows carry these attributes so you can target them from queries, tests, and styles. They are applied by the shared row renderer, so they appear on standard, virtualized, pinned, and draggable rows alike, with data-index the one exception (only DataGridTableVirtual sets it, and only on its unpinned body rows). Spacer, skeleton, empty, virtual status, and expanded-detail rows are rendered separately and carry none of them.

AttributeValueDescription
data-row-idstringThe resolved TanStack Table row id, respecting any getRowId configuration. Stable across sorting and pagination.
data-indexnumberIndex of the row inside the center (non-pinned) section. Set only by DataGridTableVirtual, for virtual measurement and stripe parity.
data-state"selected"Present while row selection is enabled and the row is selected. Omitted otherwise.
data-row-pinned"top" | "bottom"Which edge the row is pinned to. Omitted entirely when the row is not pinned.
data-row-pinned-boundary"top" | "bottom"Marks the seam between pinned and unpinned rows: the last top-pinned row, or the first bottom-pinned row.
data-depthnumberDepth of the row in a hierarchical row model (getSubRows trees or grouped rows). Omitted for root-level rows.

Header and body cells carry the pinning attributes below. They are the contract the grid's own sticky-column styling is built on, so they are also the hook to use for your own. These values were left / right before TanStack Table v9, so any CSS selecting [data-pinned="left"] needs updating to [data-pinned="start"].

AttributeValueDescription
data-pinned"start" | "end"Which edge the column is pinned to. Omitted entirely when the column is not pinned.
data-last-col"start" | "end"Marks the inner boundary of a pinned group — the last start-pinned or first end-pinned column. Carries the divider.
data-outer-pinned-col"start" | "end"Marks the outer edge of a pinned group. Header cells only; used for background clipping.

While a column is being resized the grid also renders a full-height vertical line at the pointer, with a cap in the header. It is only shown in onEnd mode — the default — because that is the mode where the column width does not move until you release, so the line is the only feedback the drag produces. Target it with [data-slot="data-grid-table-resize-indicator"]. It is positioned imperatively rather than through signals: a resize drag fires at pointer rate, and routing that through the reactive graph would touch every row on every frame.

Pinned cells stick with the CSS logical properties inset-inline-start and inset-inline-end rather than left / right, so they land on the correct edge in RTL without extra work.

On This Page

  • Installation
  • TanStack Table v9
    • No Subscribe, no memo
    • Upgrading from v8
  • Usage
  • Examples
    • Cell Border
    • Dense Table
    • Light Table
    • Striped Table
    • Auto Width
    • Row Selection
    • Tree Rows
  • Server-Side Rendering
  • API Reference
    • DataGrid
    • tableLayout
    • DataGridTableClassNames
    • DataGridContainer
    • DataGridScrollArea
    • DataGridTable
    • DataGridPagination
    • DataGridColumnHeader
    • DataGridColumnFilter
    • DataGridColumnVisibility
    • DataGridTableDnd
    • DataGridTableDndRows
    • DataGridTableVirtual
    • DataGridTableRowPin
    • DataGridTableRowSelect / DataGridTableRowSelectAll
    • DataGridTableRowExpand
    • DataGridTableFoot
    • DataGridTableFootRow
    • DataGridTableFootRowCell
    • DOM Attributes
Built by Kevin Abatan. The source code is available on GitHub.