Data Table
Powerful table and datagrids built using TanStack Table.
import type { DropdownMenuTriggerProps } from "@kobalte/core/dropdown-menu";
import type { SelectTriggerProps } from "@kobalte/core/select";
import { Badge } from "@repo/tailwindcss/ui/badge";
import { Button } from "@repo/tailwindcss/ui/button";
import { Checkbox, CheckboxControl } from "@repo/tailwindcss/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuGroupLabel,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@repo/tailwindcss/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/tailwindcss/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@repo/tailwindcss/ui/table";
import { TextField, TextFieldRoot } from "@repo/tailwindcss/ui/textfield";
import type {
Column,
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from "@tanstack/solid-table";
import {
createSolidTable,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
} from "@tanstack/solid-table";
import type { VoidProps } from "solid-js";
import {
For,
Match,
Show,
Switch,
createMemo,
createSignal,
splitProps,
} from "solid-js";
import { dataTable } from "./data-table-data";
export type Task = {
id: string;
code: string;
title: string;
status: "todo" | "in-progress" | "done" | "cancelled";
label: "bug" | "feature" | "enhancement" | "documentation";
};
const filteredStatusList = () =>
["todo", "in-progress", "done", "cancelled"].map((e) => ({
title: e,
value: e,
}));
const TableColumnHeader = <TData, TValue>(
props: VoidProps<{ column: Column<TData, TValue>; title: string }>,
) => {
const [local] = splitProps(props, ["column", "title"]);
return (
<Show
when={local.column.getCanSort() && local.column.getCanHide()}
fallback={<span class="text-sm font-medium">{local.title}</span>}
>
<div class="flex items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger
as={(props: DropdownMenuTriggerProps) => (
<Button
aria-label={
local.column.getIsSorted() === "desc"
? "Sorted descending. Click to sort ascending."
: local.column.getIsSorted() === "asc"
? "Sorted ascending. Click to sort descending."
: "Not sorted. Click to sort ascending."
}
variant="ghost"
class="-ml-4 h-8 data-[expanded]:bg-accent"
{...props}
>
<span>{local.title}</span>
<div class="ml-1">
<Switch
fallback={
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-3.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m8 9l4-4l4 4m0 6l-4 4l-4-4"
/>
</svg>
}
>
<Match when={local.column.getIsSorted() === "asc"}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-3.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-10l-4-4M8 9l4-4"
/>
</svg>
</Match>
<Match when={local.column.getIsSorted() === "desc"}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-3.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-4l-4 4m-4-4l4 4"
/>
</svg>
</Match>
</Switch>
</div>
</Button>
)}
/>
<DropdownMenuContent>
<Show when={local.column.getCanSort()}>
<DropdownMenuItem
aria-label="Sort ascending"
onClick={() => local.column.toggleSorting(false, true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4 text-muted-foreground/70"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-10l-4-4M8 9l4-4"
/>
</svg>
Asc
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Sort descending"
onClick={() => local.column.toggleSorting(true, true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4 text-muted-foreground/70"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-4l-4 4m-4-4l4 4"
/>
</svg>
Desc
</DropdownMenuItem>
</Show>
<Show when={local.column.getCanSort() && local.column.getCanHide()}>
<DropdownMenuSeparator />
</Show>
<Show when={local.column.getCanHide()}>
<DropdownMenuItem
aria-label="Hide column"
onClick={() => local.column.toggleVisibility(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4 text-muted-foreground/70"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 9c-2.4 2.667-5.4 4-9 4c-3.6 0-6.6-1.333-9-4m0 6l2.5-3.8M21 14.976L18.508 11.2M9 17l.5-4m5.5 4l-.5-4"
/>
</svg>
Hide
</DropdownMenuItem>
</Show>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Show>
);
};
const columns: ColumnDef<Task>[] = [
{
id: "selects",
header: (props) => (
<Checkbox
indeterminate={props.table.getIsSomePageRowsSelected()}
checked={props.table.getIsAllPageRowsSelected()}
onChange={(value) => props.table.toggleAllPageRowsSelected(value)}
aria-label="Select all"
class="translate-y-[2px]"
>
<CheckboxControl />
</Checkbox>
),
cell: (props) => (
<Checkbox
checked={props.row.getIsSelected()}
onChange={(value) => props.row.toggleSelected(value)}
aria-label="Select row"
class="translate-y-[2px]"
>
<CheckboxControl />
</Checkbox>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "code",
header: (props) => <TableColumnHeader column={props.column} title="Task" />,
cell: (props) => <div class="w-[70px]">{props.row.getValue("code")}</div>,
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "title",
header: (props) => (
<TableColumnHeader column={props.column} title="Title" />
),
cell: (props) => (
<div class="flex space-x-2">
<Badge variant="outline">{props.row.original.label}</Badge>
<span class="max-w-[250px] truncate font-medium">
{props.row.getValue("title")}
</span>
</div>
),
},
{
accessorKey: "status",
header: (props) => (
<TableColumnHeader column={props.column} title="Status" />
),
cell: (props) => (
<div class="flex w-[100px] items-center">
<Switch>
<Match when={props.row.original.status === "cancelled"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 20.777a8.942 8.942 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223M14 14l-4-4m0 4l4-4"
/>
</svg>
</Match>
<Match when={props.row.original.status === "done"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M10 20.777a8.942 8.942 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223" />
<path d="m9 12l2 2l4-4" />
</g>
</svg>
</Match>
<Match when={props.row.original.status === "in-progress"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 20.777a8.942 8.942 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223M12 9l-2 3h4l-2 3"
/>
</svg>
</Match>
<Match when={props.row.original.status === "todo"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12 16v.01M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483M10 20.777a8.942 8.942 0 0 1-2.48-.969" />
<path d="M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223" />
</g>
</svg>
</Match>
</Switch>
<span class="capitalize">{props.row.original.status}</span>
</div>
),
filterFn: (row, id, value) => {
return Array.isArray(value) && value.includes(row.getValue(id));
},
},
{
id: "actions",
cell: () => (
<DropdownMenu placement="bottom-end">
<DropdownMenuTrigger class="flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
/>
<title>Action</title>
</svg>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const DataTableDemo = () => {
const data = createMemo(() => dataTable);
const [rowSelection, setRowSelection] = createSignal({});
const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>(
{},
);
const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>(
[],
);
const [sorting, setSorting] = createSignal<SortingState>([]);
const table = createSolidTable({
get data() {
return data();
},
columns,
state: {
get sorting() {
return sorting();
},
get columnVisibility() {
return columnVisibility();
},
get rowSelection() {
return rowSelection();
},
get columnFilters() {
return columnFilters();
},
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<div class="w-full space-y-2.5">
<div class="flex items-center justify-between gap-2">
<TextFieldRoot>
<TextField
type="text"
placeholder="Filter titles..."
class="h-8"
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
onInput={(e) =>
table.getColumn("title")?.setFilterValue(e.currentTarget.value)
}
/>
</TextFieldRoot>
<div class="flex items-center gap-2">
<Select
onChange={(e) => {
table
.getColumn("status")
?.setFilterValue(e.length ? e.map((v) => v.value) : undefined);
}}
placement="bottom-end"
sameWidth={false}
options={filteredStatusList()}
optionValue="value"
optionTextValue="title"
multiple
itemComponent={(props) => (
<SelectItem item={props.item} class="capitalize">
{props.item.rawValue.title}
</SelectItem>
)}
>
<SelectTrigger
as={(props: SelectTriggerProps) => (
<Button
{...props}
aria-label="Filter status"
variant="outline"
class="relative flex h-8 w-full gap-2 [&>svg]:hidden"
>
<div class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m12 20l-3 1v-8.5L4.52 7.572A2 2 0 0 1 4 6.227V4h16v2.172a2 2 0 0 1-.586 1.414L15 12v1.5m2.001 5.5a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-3.5V17m0 4v1.5m3.031-5.25l-1.299.75m-3.463 2l-1.3.75m0-3.5l1.3.75m3.463 2l1.3.75"
/>
<title>Status</title>
</svg>
Status
</div>
<SelectValue<
ReturnType<typeof filteredStatusList>[0]
> class="flex h-full items-center gap-1">
{(state) => (
<Show
when={state.selectedOptions().length <= 2}
fallback={
<>
<Badge class="absolute -top-2 right-0 block size-4 rounded-full p-0 capitalize md:hidden">
{state.selectedOptions().length}
</Badge>
<Badge class="hidden capitalize md:inline-flex py-0 px-1">
{state.selectedOptions().length} selected
</Badge>
</>
}
>
<For each={state.selectedOptions()}>
{(item) => (
<>
<Badge class="absolute -top-2 right-0 block size-4 rounded-full p-0 capitalize md:hidden">
{state.selectedOptions().length}
</Badge>
<Badge class="hidden capitalize md:inline-flex py-0 px-1">
{item.title}
</Badge>
</>
)}
</For>
</Show>
)}
</SelectValue>
</Button>
)}
/>
<SelectContent />
</Select>
<DropdownMenu placement="bottom-end">
<DropdownMenuTrigger
as={(props: DropdownMenuTriggerProps) => (
<Button
{...props}
aria-label="Toggle columns"
variant="outline"
class="flex h-8"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0" />
<path d="M12 18c-3.6 0-6.6-2-9-6c2.4-4 5.4-6 9-6c3.6 0 6.6 2 9 6m-3.999 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-3.5V17m0 4v1.5m3.031-5.25l-1.299.75m-3.463 2l-1.3.75m0-3.5l1.3.75m3.463 2l1.3.75" />
</g>
<title>View</title>
</svg>
View
</Button>
)}
/>
<DropdownMenuContent class="w-40">
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Toggle columns</DropdownMenuGroupLabel>
<DropdownMenuSeparator />
<For
each={table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" &&
column.getCanHide(),
)}
>
{(column) => (
<DropdownMenuCheckboxItem
class="capitalize"
checked={column.getIsVisible()}
onChange={(value) => column.toggleVisibility(value)}
>
<span class="truncate">{column.id}</span>
</DropdownMenuCheckboxItem>
)}
</For>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<For each={table.getHeaderGroups()}>
{(headerGroup) => (
<TableRow>
<For each={headerGroup.headers}>
{(header) => {
return (
<TableHead>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
}}
</For>
</TableRow>
)}
</For>
</TableHeader>
<TableBody>
<Show
when={table.getRowModel().rows?.length}
fallback={
<TableRow>
<TableCell colSpan={columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
}
>
<For each={table.getRowModel().rows}>
{(row) => (
<TableRow data-state={row.getIsSelected() && "selected"}>
<For each={row.getVisibleCells()}>
{(cell) => (
<TableCell>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
)}
</For>
</TableRow>
)}
</For>
</Show>
</TableBody>
</Table>
</div>
<div class="flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto px-2 py-1 sm:flex-row">
<div class="flex-1 whitespace-nowrap text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div class="flex flex-col-reverse items-center gap-4 sm:flex-row">
<div class="flex items-center space-x-2">
<p class="whitespace-nowrap text-sm font-medium">Rows per page</p>
<Select
value={table.getState().pagination.pageSize}
onChange={(value) => value && table.setPageSize(value)}
options={[10, 20, 30, 40, 50]}
itemComponent={(props) => (
<SelectItem item={props.item}>{props.item.rawValue}</SelectItem>
)}
>
<SelectTrigger class="h-8 w-[4.5rem]">
<SelectValue<string>>
{(state) => state.selectedOption()}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
</div>
<div class="flex items-center justify-center whitespace-nowrap text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div class="flex items-center space-x-2">
<Button
aria-label="Go to first page"
variant="outline"
class="flex size-8 p-0"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m11 7l-5 5l5 5m6-10l-5 5l5 5"
/>
</svg>
</Button>
<Button
aria-label="Go to previous page"
variant="outline"
size="icon"
class="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m15 6l-6 6l6 6"
/>
</svg>
</Button>
<Button
aria-label="Go to next page"
variant="outline"
size="icon"
class="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
</svg>
</Button>
<Button
aria-label="Go to last page"
variant="outline"
size="icon"
class="flex size-8"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m7 7l5 5l-5 5m6-10l5 5l-5 5"
/>
</svg>
</Button>
</div>
</div>
</div>
</div>
);
};
export default DataTableDemo;
Introduction
Every data table or datagrid I’ve created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
It doesn’t make sense to combine all of these variations into a single component. If we do that, we’ll lose the flexibility that headless UI provides.
So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.
We’ll start with the basic <Table />
component and build a complex data table from scratch.
Tip: If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.
Table of Contents
This guide will show you how to use TanStack Table and the <Table />
component to build your own custom data table. We’ll cover the following topics:
Installation
- Add the
<Table />
component to your project:
npx shadcn-solid@latest add table button dropdown-menu textfield checkbox
- Add
tanstack/solid-table
dependency:
npm install @tanstack/solid-table
Prerequisites
We are going to build a table to show tasks. Here’s what our data looks like:
type Task = {
id: string;
code: string;
title: string;
status: "todo" | "in-progress" | "done" | "cancelled";
label: "bug" | "feature" | "enhancement" | "documentation";
};
export const tasks: Task[] = [
{
id: "ptL0KpX_yRMI98JFr6B3n",
code: "TASK-33",
title: "We need to bypass the redundant AI interface!",
status: "todo",
label: "bug"
},
{
id: "RsrTg_SmBKPKwbUlr7Ztv",
code: "TASK-59",
title:
"Overriding the capacitor won't do anything, we need to generate the solid state JBOD pixel!",
status: "in-progress",
label: "feature"
}
// ...
];
Project Structure
Start by creating the following file structure:
src
└── routes
├── _components
│ ├── columns.tsx
│ └── data-table.tsx
└── index.tsx
-
columns.tsx
will contain our column definitions. -
data-table.tsx
will contain our<DataTable />
component. -
index.tsx
is where we’ll fetch data and render our table.
Basic Table
Let’s start by building a basic table.
Column Definitions
First, we’ll define our columns.
import type { ColumnDef } from "@tanstack/solid-table";
// This type is used to define the shape of our data.
// You can use a Zod or Validbot schema here if you want.
export type Task = {
id: string;
code: string;
title: string;
status: "todo" | "in-progress" | "done" | "cancelled";
label: "bug" | "feature" | "enhancement" | "documentation";
};
export const columns: ColumnDef<Task>[] = [
{
accessorKey: "code",
header: "Task"
},
{
accessorKey: "title",
header: "Title"
},
{
accessorKey: "status",
header: "Status"
}
];
<DataTable />
component
Next, we’ll create a <DataTable />
component to render our table.
import type { ColumnDef } from "@tanstack/solid-table";
import { flexRender, createSolidTable, getCoreRowModel } from "@tanstack/solid-table";
import { For, Show, splitProps, Accessor } from "solid-js";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
type Props<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: Accessor<TData[] | undefined>;
};
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
const [local] = splitProps(props, ["columns", "data"]);
const table = createSolidTable({
get data() {
return local.data() || [];
},
columns: local.columns,
getCoreRowModel: getCoreRowModel()
});
return (
<div class="rounded-md border">
<Table>
<TableHeader>
<For each={table.getHeaderGroups()}>
{headerGroup => (
<TableRow>
<For each={headerGroup.headers}>
{header => {
return (
<TableHead>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
}}
</For>
</TableRow>
)}
</For>
</TableHeader>
<TableBody>
<Show
when={table.getRowModel().rows?.length}
fallback={
<TableRow>
<TableCell colSpan={local.columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
}
>
<For each={table.getRowModel().rows}>
{row => (
<TableRow data-state={row.getIsSelected() && "selected"}>
<For each={row.getVisibleCells()}>
{cell => (
<TableCell>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
)}
</For>
</TableRow>
)}
</For>
</Show>
</TableBody>
</Table>
</div>
);
};
Render the table
Finally, we’ll render our table in our index page.
import type { Task } from "./_components/columns";
import { columns } from "./_components/columns";
import { DataTable } from "./_components/data-table";
import type { RouteDefinition } from "@solidjs/router";
import { cache, createAsync } from "@solidjs/router";
const getData = cache(async (): Promise<Task[]> => {
// Fetch data from your API here.
return [
{
id: "ptL0KpX_yRMI98JFr6B3n",
code: "TASK-33",
title: "We need to bypass the redundant AI interface!",
status: "todo",
label: "bug"
}
// ...
];
}, "data");
export const route: RouteDefinition = {
load: () => getData()
};
const Home = () => {
const data = createAsync(() => getData());
return (
<div class="w-full space-y-2.5">
<DataTable columns={columns} data={data} />
</div>
);
};
export default Home;
Row Actions
Update our columns definition to add a new actions
column. The actions
cell returns a <Dropdown />
component.
//...
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
export const columns: ColumnDef<Task>[] = [
// ...
{
id: "actions",
cell: () => (
<DropdownMenu placement="bottom-end">
<DropdownMenuTrigger class="flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
/>
</svg>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
// ...
];
You can access the row data using row.original
in the cell
function. Use this to handle actions for your row eg. use the id
to make a DELETE call to your API.
Pagination
Next, we’ll add pagination to our table.
Update <DataTable>
//...
import {
//...
getPaginationRowModel
} from "@tanstack/solid-table";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
const table = createSolidTable({
//...
getPaginationRowModel: getPaginationRowModel()
});
// ...
};
This will automatically paginate your rows into pages of 10. See the pagination docs for more information on customizing page size and implementing manual pagination.
Add pagination controls
We can add pagination controls to our table using the <Button />
component and the table.previousPage()
, table.nextPage()
API methods.
//...
import { Button } from "@/components/ui/button"
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) {
//...
return (
<div>
<div class="rounded-md border">...
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}
Sorting
Update <DataTable>
//...
import {
//...
getSortedRowModel
} from "@tanstack/solid-table";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
const [sorting, setSorting] = createSignal<SortingState>([]);
const table = createSolidTable({
//...
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: {
get sorting() {
return sorting();
}
}
});
// ...
};
Make header cell sortable
Update the status
header cell to add sorting controls.
//...
import { Button } from "@/components/ui/button";
export const columns: ColumnDef<Task>[] = [
// ...
{
accessorKey: "status",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-2 size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-4l-4 4m-4-4l4 4"
/>
</svg>
</Button>
);
}
}
// ...
];
Filtering
Add filter to the title. See the filtering docs for more information on customizing filters.
Update <DataTable>
//...
import {
//...
getFilteredRowModel
} from "@tanstack/solid-table";
import { TextField, TextFieldInput } from "~/components/ui/textfield";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
//...
const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([]);
const table = createSolidTable({
//...
getFilteredRowModel: getFilteredRowModel(),
state: {
//...
get columnFilters() {
return columnFilters();
}
}
});
<div>
<div class="flex items-center py-4">
<TextField>
<TextFieldInput
placeholder="Filter title..."
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
onInput={event => table.getColumn("title")?.setFilterValue(event.currentTarget.value)}
class="max-w-sm"
/>
</TextField>
</div>
<div class="rounded-md border">...</div>
</div>;
};
Visibility
Adding column visibility using visibility API.
Update <DataTable>
//...
import { As } from "@kobalte/core";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
//...
const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>({});
const table = createSolidTable({
//...
onColumnVisibilityChange: setColumnVisibility,
state: {
//...
get columnVisibility() {
return columnVisibility();
}
}
});
<div>
<div class="flex items-center py-4">
//...
<DropdownMenu>
<DropdownMenuTrigger asChild>
<As component={Button} variant="outline" class="ml-auto">
Columns
</As>
</DropdownMenuTrigger>
<DropdownMenuContent>
<For each={table.getAllColumns().filter(column => column.getCanHide())}>
{item => (
<DropdownMenuCheckboxItem
class="capitalize"
checked={item.getIsVisible()}
onChange={value => item.toggleVisibility(!!value)}
>
{item.id}
</DropdownMenuCheckboxItem>
)}
</For>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border">...</div>
</div>;
};
Row Selection
Next, we’re going to add row selection to our table.
Update column definitions
//...
import { Checkbox, CheckboxControl } from "@/components/ui/checkbox";
export const columns: ColumnDef<Task>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
indeterminate={table.getIsSomePageRowsSelected()}
checked={table.getIsAllPageRowsSelected()}
onChange={value => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
>
<CheckboxControl />
</Checkbox>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onChange={value => row.toggleSelected(!!value)}
aria-label="Select row"
>
<CheckboxControl />
</Checkbox>
),
enableSorting: false,
enableHiding: false
}
// ...
];
Update <DataTable>
//...
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
//...
const [rowSelection, setRowSelection] = createSignal({});
const table = createSolidTable({
//...
onRowSelectionChange: setRowSelection,
state: {
// ...
get rowSelection() {
return rowSelection();
}
}
});
<div>...</div>;
};
Show selected rows
You can show the number of selected rows using the table.getFilteredSelectedRowModel()
API.
<div class="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length}{" "}
row(s) selected.
</div>