Data Table
Powerful table and datagrids built using TanStack Table.
<script lang="ts">
import ChevronDown from "lucide-svelte/icons/chevron-down";
import {
type ColumnDef,
type ColumnFiltersState,
type PaginationState,
type RowSelectionState,
type SortingState,
type VisibilityState,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel
} from "@tanstack/table-core";
import { createRawSnippet } from "svelte";
import DataTableCheckbox from "./data-table/data-table-checkbox.svelte";
import DataTableEmailButton from "./data-table/data-table-email-button.svelte";
import DataTableActions from "./data-table/data-table-actions.svelte";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import {
FlexRender,
createSvelteTable,
renderComponent,
renderSnippet
} from "$lib/components/ui/data-table/index.js";
type Payment = {
id: string;
amount: number;
status: "Pending" | "Processing" | "Success" | "Failed";
email: string;
};
const data: Payment[] = [
{
id: "m5gr84i9",
amount: 316,
status: "Success",
email: "ken99@yahoo.com"
},
{
id: "3u1reuv4",
amount: 242,
status: "Success",
email: "Abe45@gmail.com"
},
{
id: "derv1ws0",
amount: 837,
status: "Processing",
email: "Monserrat44@gmail.com"
},
{
id: "5kma53ae",
amount: 874,
status: "Success",
email: "Silas22@gmail.com"
},
{
id: "bhqecj4p",
amount: 721,
status: "Failed",
email: "carmella@hotmail.com"
}
];
const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) =>
renderComponent(DataTableCheckbox, {
checked: table.getIsAllPageRowsSelected(),
indeterminate:
table.getIsSomePageRowsSelected() &&
!table.getIsAllPageRowsSelected(),
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
"aria-label": "Select all"
}),
cell: ({ row }) =>
renderComponent(DataTableCheckbox, {
checked: row.getIsSelected(),
onCheckedChange: (value) => row.toggleSelected(!!value),
"aria-label": "Select row"
}),
enableSorting: false,
enableHiding: false
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const statusSnippet = createRawSnippet<[string]>((getStatus) => {
const status = getStatus();
return {
render: () => `<div class="capitalize">${status}</div>`
};
});
return renderSnippet(statusSnippet, row.getValue("status"));
}
},
{
accessorKey: "email",
header: ({ column }) =>
renderComponent(DataTableEmailButton, {
onclick: () => column.toggleSorting(column.getIsSorted() === "asc")
}),
cell: ({ row }) => {
const emailSnippet = createRawSnippet<[string]>((getEmail) => {
const email = getEmail();
return {
render: () => `<div class="lowercase">${email}</div>`
};
});
return renderSnippet(emailSnippet, row.getValue("email"));
}
},
{
accessorKey: "amount",
header: () => {
const amountHeaderSnippet = createRawSnippet(() => {
return {
render: () => `<div class="text-right">Amount</div>`
};
});
return renderSnippet(amountHeaderSnippet, "");
},
cell: ({ row }) => {
const amountCellSnippet = createRawSnippet<[string]>((getAmount) => {
const amount = getAmount();
return {
render: () => `<div class="text-right font-medium">${amount}</div>`
};
});
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
return renderSnippet(
amountCellSnippet,
formatter.format(Number.parseFloat(row.getValue("amount")))
);
}
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) =>
renderComponent(DataTableActions, { id: row.original.id })
}
];
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
let rowSelection = $state<RowSelectionState>({});
let columnVisibility = $state<VisibilityState>({});
const table = createSvelteTable({
get data() {
return data;
},
columns,
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get columnVisibility() {
return columnVisibility;
},
get rowSelection() {
return rowSelection;
},
get columnFilters() {
return columnFilters;
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
onColumnVisibilityChange: (updater) => {
if (typeof updater === "function") {
columnVisibility = updater(columnVisibility);
} else {
columnVisibility = updater;
}
},
onRowSelectionChange: (updater) => {
if (typeof updater === "function") {
rowSelection = updater(rowSelection);
} else {
rowSelection = updater;
}
}
});
</script>
<div class="w-full">
<div class="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
oninput={(e) =>
table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
onchange={(e) => {
table.getColumn("email")?.setFilterValue(e.currentTarget.value);
}}
class="max-w-sm"
/>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="ml-auto">
Columns <ChevronDown />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#each table
.getAllColumns()
.filter((col) => col.getCanHide()) as column}
<DropdownMenu.CheckboxItem
class="capitalize"
bind:checked={() => column.getIsVisible(),
(v) => column.toggleVisibility(!!v)}
>
{column.id}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="[&:has([role=checkbox])]:pl-3">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="[&:has([role=checkbox])]:pl-3">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No results.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 pt-4">
<div class="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div class="space-x-2">
<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>
</div>
<script lang="ts">
import ChevronDown from "lucide-svelte/icons/chevron-down";
import {
type ColumnDef,
type ColumnFiltersState,
type PaginationState,
type RowSelectionState,
type SortingState,
type VisibilityState,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel
} from "@tanstack/table-core";
import { createRawSnippet } from "svelte";
import DataTableCheckbox from "./data-table/data-table-checkbox.svelte";
import DataTableEmailButton from "./data-table/data-table-email-button.svelte";
import DataTableActions from "./data-table/data-table-actions.svelte";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import {
FlexRender,
createSvelteTable,
renderComponent,
renderSnippet
} from "$lib/components/ui/data-table/index.js";
type Payment = {
id: string;
amount: number;
status: "Pending" | "Processing" | "Success" | "Failed";
email: string;
};
const data: Payment[] = [
{
id: "m5gr84i9",
amount: 316,
status: "Success",
email: "ken99@yahoo.com"
},
{
id: "3u1reuv4",
amount: 242,
status: "Success",
email: "Abe45@gmail.com"
},
{
id: "derv1ws0",
amount: 837,
status: "Processing",
email: "Monserrat44@gmail.com"
},
{
id: "5kma53ae",
amount: 874,
status: "Success",
email: "Silas22@gmail.com"
},
{
id: "bhqecj4p",
amount: 721,
status: "Failed",
email: "carmella@hotmail.com"
}
];
const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) =>
renderComponent(DataTableCheckbox, {
checked: table.getIsAllPageRowsSelected(),
indeterminate:
table.getIsSomePageRowsSelected() &&
!table.getIsAllPageRowsSelected(),
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
"aria-label": "Select all"
}),
cell: ({ row }) =>
renderComponent(DataTableCheckbox, {
checked: row.getIsSelected(),
onCheckedChange: (value) => row.toggleSelected(!!value),
"aria-label": "Select row"
}),
enableSorting: false,
enableHiding: false
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const statusSnippet = createRawSnippet<[string]>((getStatus) => {
const status = getStatus();
return {
render: () => `<div class="capitalize">${status}</div>`
};
});
return renderSnippet(statusSnippet, row.getValue("status"));
}
},
{
accessorKey: "email",
header: ({ column }) =>
renderComponent(DataTableEmailButton, {
onclick: () => column.toggleSorting(column.getIsSorted() === "asc")
}),
cell: ({ row }) => {
const emailSnippet = createRawSnippet<[string]>((getEmail) => {
const email = getEmail();
return {
render: () => `<div class="lowercase">${email}</div>`
};
});
return renderSnippet(emailSnippet, row.getValue("email"));
}
},
{
accessorKey: "amount",
header: () => {
const amountHeaderSnippet = createRawSnippet(() => {
return {
render: () => `<div class="text-right">Amount</div>`
};
});
return renderSnippet(amountHeaderSnippet, "");
},
cell: ({ row }) => {
const amountCellSnippet = createRawSnippet<[string]>((getAmount) => {
const amount = getAmount();
return {
render: () => `<div class="text-right font-medium">${amount}</div>`
};
});
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
return renderSnippet(
amountCellSnippet,
formatter.format(Number.parseFloat(row.getValue("amount")))
);
}
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) =>
renderComponent(DataTableActions, { id: row.original.id })
}
];
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
let rowSelection = $state<RowSelectionState>({});
let columnVisibility = $state<VisibilityState>({});
const table = createSvelteTable({
get data() {
return data;
},
columns,
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get columnVisibility() {
return columnVisibility;
},
get rowSelection() {
return rowSelection;
},
get columnFilters() {
return columnFilters;
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
onColumnVisibilityChange: (updater) => {
if (typeof updater === "function") {
columnVisibility = updater(columnVisibility);
} else {
columnVisibility = updater;
}
},
onRowSelectionChange: (updater) => {
if (typeof updater === "function") {
rowSelection = updater(rowSelection);
} else {
rowSelection = updater;
}
}
});
</script>
<div class="w-full">
<div class="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
oninput={(e) =>
table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
onchange={(e) => {
table.getColumn("email")?.setFilterValue(e.currentTarget.value);
}}
class="max-w-sm"
/>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="ml-auto">
Columns <ChevronDown class="ml-2 size-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#each table
.getAllColumns()
.filter((col) => col.getCanHide()) as column}
<DropdownMenu.CheckboxItem
class="capitalize"
bind:checked={() => column.getIsVisible(),
(v) => column.toggleVisibility(!!v)}
>
{column.id}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="[&:has([role=checkbox])]:pl-3">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="[&:has([role=checkbox])]:pl-3">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No results.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 pt-4">
<div class="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div class="space-x-2">
<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>
</div>
Introduction
Data tables are difficult to componentize because of the wide variety of features they support, and the uniqueness of every data set.
So instead of trying to create a one-size-fits-all solution, we've created a guide to help you build your own data tables.
We'll start with the basic <Table />
component, and work our way up to a fully-featured data table.
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 along with thedata-table
helpers. These helpers enable TanStack Table v8 to work with Svelte 5 Snippets, Components, etc.
npx shadcn-svelte@next add table data-table
- Add
@tanstack/table-core
as a dependency:
npm i @tanstack/table-core
Prerequisites
We're going to build a table to show recent payments. Here's what our data looks like:
type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const data: Payment[] = [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "m@example.com",
},
{
id: "489e1d42",
amount: 125,
status: "processing",
email: "example@gmail.com",
},
// ...
];
Project Structure
Start by creating a route where your data table will live (we'll call ours payments), along with the following files:
routes
└── payments
├── columns.ts
├── data-table.svelte
├── data-table-actions.svelte
├── data-table-checkbox.svelte
├── data-table-email-button.svelte
└── +page.svelte
columns.ts
will contain our column definitions.data-table.svelte
will contain the<Table />
component and the complete<DataTable />
component.data-table-actions.svelte
will contain the actions menu for each row.data-table-checkbox.svelte
will contain the checkbox for each row.data-table-email-button.svelte
will contain the sortable email header button.+page.svelte
is where we'll render and access<DataTable />
component.
Basic Table
Let's start by building a basic table.
Column Definitions
First, we'll define our columns.
import type { ColumnDef } from "@tanstack/table-core";
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "amount",
header: "Amount",
},
];
Note: Columns are where you define the core of what your table will look like. They define the data that will be displayed, how it will be formatted, sorted and filtered.
<DataTable />
Component
Next, we'll create a <DataTable />
component to render our table.
<script lang="ts" generics="TData, TValue">
import { type ColumnDef, getCoreRowModel } from "@tanstack/table-core";
import {
createSvelteTable,
FlexRender,
} from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
};
let { data, columns }: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
});
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No results.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
Tip: If you find yourself using <DataTable />
in multiple places, this is the component you could make reusable by extracting it to components/ui/data-table.svelte
.
<DataTable columns={columns} data={data} />
Render the table
Finally, we'll render our table in our page component.
export async function load() {
// logic to fetch payments data here
const payments = await getPayments();
return {
payments,
};
}
<script lang="ts">
import DataTable from "./data-table.svelte";
import { columns } from "./columns.js";
let { data } = $props();
</script>
<DataTable {data} {columns} />
Cell Formatting
Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.
Update columns definition
Update the header
and cell
definitions for amount as follows:
import type { ColumnDef } from "@tanstack/table-core";
import { createRawSnippet } from "svelte";
import { renderSnippet } from "$lib/components/ui/data-table/index.js";
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "amount",
header: () => {
const amountHeaderSnippet = createRawSnippet(() => ({
render: () => `<div class="text-right">Amount</div>`,
}));
return renderSnippet(amountHeaderSnippet, "");
},
cell: ({ row }) => {
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const amountCellSnippet = createRawSnippet<[string]>((getAmount) => {
const amount = getAmount();
return {
render: () => `<div class="text-right font-medium">${amount}</div>`,
};
});
return renderSnippet(
amountCellSnippet,
formatter.format(parseFloat(row.getValue("amount")))
);
},
},
];
We're using the createRawSnippet
function to create a Svelte Snippet for rendering simple HTML elements that don't require full lifecycle and state capabilities like a component. We then use the renderSnippet
helper function to render the snippet.
You can use the same approach to format other cells and headers.
Row Actions
Let's add row actions to our table. We'll use the <DropdownMenu />
component for this.
Create actions component
We'll start by defining the actions menu in our data-table-actions.svelte
component.
<script lang="ts">
import Ellipsis from "lucide-svelte/icons/ellipsis";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
let { id }: { id: string } = $props();
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="icon"
class="relative size-8 p-0"
>
<span class="sr-only">Open menu</span>
<Ellipsis />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.GroupHeading>Actions</DropdownMenu.GroupHeading>
<DropdownMenu.Item onclick={() => navigator.clipboard.writeText(id)}>
Copy payment ID
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item>View customer</DropdownMenu.Item>
<DropdownMenu.Item>View payment details</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
Update columns definition
Now that we've defined the <DataTableActions />
component, let's update our actions
column definition to use it.
import type { ColumnDef } from "@tanstack/table-core";
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableActions from "./data-table-actions.svelte";
export const columns: ColumnDef<Payment>[] = [
// ...
{
id: "actions",
cell: ({ row }) => {
// You can pass whatever you need from `row.original` to the component
return renderComponent(DataTableActions, { id: row.original.id });
},
},
];
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 />
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
type PaginationState,
getCoreRowModel,
getPaginationRowModel,
} from "@tanstack/table-core";
type DataTableProps<TData, TValue> = {
data: TData[];
columns: ColumnDef<TData, TValue>[];
};
let { data, columns }: DataTableProps<TData, TValue> = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
const table = createSvelteTable({
get data() {
return data;
},
columns,
state: {
get pagination() {
return pagination;
},
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
</script>
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.
Adding pagination controls
We can add pagination controls to our table using the <Button />
component and the table.previousPage()
, table.nextPage()
API methods.
<script lang="ts" generics="TData, TValue">
import { Button } from "$lib/components/ui/button/index.js";
let { columns, data }: DataTableProps<TData, TValue> = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
</script>
<div>
<div class="rounded-md border">
<Table.Root>
<!--- ... table implementation -->
</Table.Root>
</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>
See Reusable Components section for a more advanced pagination component.
Sorting
Let's make the email column sortable.
Define <DataTableEmailButton />
component
We'll start by creating a component to render a sortable email header button.
<script lang="ts">
import type { ComponentProps } from "svelte";
import ArrowUpDown from "lucide-svelte/icons/arrow-up-down";
import { Button } from "$lib/components/ui/button/index.js";
let { variant = "ghost", ...restProps }: ComponentProps<typeof Button> =
$props();
</script>
<Button {variant} {...restProps}>
Email
<ArrowUpDown class="ml-2" />
</Button>
Update <DataTable />
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
type PaginationState,
type SortingState,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
} from "@tanstack/table-core";
let { columns, data }: DataTableProps<TData, TValue> = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
},
});
</script>
Make header cell sortable
We can now update the email
header cell to add sorting controls.
import type { ColumnDef } from "@tanstack/table-core";
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableEmailButton from "./data-table-email-button.svelte";
export const columns: ColumnDef<Payment>[] = [
// ...
{
accessorKey: "email",
header: ({ column }) =>
renderComponent(DataTableEmailButton, {
onclick: () => column.toggleSorting(column.getIsSorted() === "asc"),
}),
},
];
This will automatically sort the table (asc and desc) when the user toggles on the header cell.
Filtering
Let's add a search input to filter emails in our table.
Update <DataTable />
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
type PaginationState,
type SortingState,
type ColumnFiltersState,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
} from "@tanstack/table-core";
import { Input } from "$lib/components/ui/input/index.js";
let { columns, data }: DataTableProps<TData, TValue> = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get columnFilters() {
return columnFilters;
},
},
});
</script>
<div>
<div class="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onchange={(e) => {
table.getColumn("email")?.setFilterValue(e.currentTarget.value);
}}
oninput={(e) => {
table.getColumn("email")?.setFilterValue(e.currentTarget.value);
}}
class="max-w-sm"
/>
</div>
<div class="rounded-md border">
<Table.Root><!-- ... --></Table.Root>
</div>
</div>
Filtering is now enabled for the email
column. You can add filters to other columns as well. See the filtering docs for more information on customizing filters.
Visibility
Adding column visibility is fairly simple using @tanstack/table-core
visibility API.
Update <DataTable />
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
type PaginationState,
type SortingState,
type ColumnFiltersState,
type VisibilityState,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
} from "@tanstack/table-core";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
let { columns, data }: DataTableProps<TData, TValue> = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
let columnVisibility = $state<VisibilityState>({});
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
onColumnVisibilityChange: (updater) => {
if (typeof updater === "function") {
columnVisibility = updater(columnVisibility);
} else {
columnVisibility = updater;
}
},
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get columnFilters() {
return columnFilters;
},
get columnVisibility() {
return columnVisibility;
},
},
});
</script>
<div>
<div class="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={table.getColumn("email")?.getFilterValue() as string}
onchange={(e) =>
table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
oninput={(e) =>
table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
class="max-w-sm"
/>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="ml-auto">Columns</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#each table
.getAllColumns()
.filter((col) => col.getCanHide()) as column (column.id)}
<DropdownMenu.CheckboxItem
class="capitalize"
bind:checked={() => column.getIsVisible(),
(v) => column.toggleVisibility(!!v)}
>
{column.id}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div class="rounded-md border">
<Table.Root><!--...--></Table.Root>
</div>
</div>
This adds a dropdown menu that you can use to toggle column visibility.
Row Selection
Next, we're going to add row selection to our table.
Define <DataTableCheckbox />
component
We'll start by defining the checkbox component in our data-table-checkbox.svelte
component.
<script lang="ts">
import type { ComponentProps } from "svelte";
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
let {
checked = false,
onCheckedChange = (v) => (checked = v),
...restProps
}: ComponentProps<typeof Checkbox> = $props();
</script>
<Checkbox bind:checked={() => checked, onCheckedChange} {...restProps} />
Update columns definition
Now that we have a new component, we can add a select
column definition to render a checkbox.
import type { ColumnDef } from "@tanstack/table-core";
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
export const columns: ColumnDef<Payment>[] = [
// ...
{
id: "select",
header: ({ table }) =>
renderComponent(Checkbox, {
checked: table.getIsAllPageRowsSelected(),
indeterminate:
table.getIsSomePageRowsSelected() &&
!table.getIsAllPageRowsSelected(),
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
"aria-label": "Select all",
}),
cell: ({ row }) =>
renderComponent(Checkbox, {
checked: row.getIsSelected(),
onCheckedChange: (value) => row.toggleSelected(!!value),
"aria-label": "Select row",
}),
enableSorting: false,
enableHiding: false,
},
];
Update <DataTable />
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
type PaginationState,
type SortingState,
type ColumnFiltersState,
type VisibilityState,
type RowSelectionState,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
} from "@tanstack/table-core";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
let { columns, data }: DataTableProps<TData, TValue> = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
let columnVisibility = $state<VisibilityState>({});
let rowSelection = $state<RowSelectionState>({});
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
onColumnVisibilityChange: (updater) => {
if (typeof updater === "function") {
columnVisibility = updater(columnVisibility);
} else {
columnVisibility = updater;
}
},
onRowSelectionChange: (updater) => {
if (typeof updater === "function") {
rowSelection = updater(rowSelection);
} else {
rowSelection = updater;
}
},
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get columnFilters() {
return columnFilters;
},
get columnVisibility() {
return columnVisibility;
},
get rowSelection() {
return rowSelection;
},
},
});
</script>
This adds a checkbox to each row and a checkbox in the header to select all rows.
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>
Reusable Components
Check out the Tasks example to learn about creating reusable components for your data tables.
On This Page