< script lang = "ts" >
import ChevronDown from "svelte-radix/ChevronDown.svelte" ;
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 () ||
(table. getIsSomePageRowsSelected () && "indeterminate" ),
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"
controlledChecked
checked ={column. getIsVisible ()}
onCheckedChange ={( value ) => column. toggleVisibility ( !! value)}
>
{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 >
Copy
< 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 () ||
(table. getIsSomePageRowsSelected () && "indeterminate" ),
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"
controlledChecked
checked ={column. getIsVisible ()}
onCheckedChange ={( value ) => column. toggleVisibility ( !! value)}
>
{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 >
Copy
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.
Tip: If you find yourself using the same table in multiple places, 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 along with the data-table
helpers. These helpers enable TanStack Table v8 to work with Svelte 5 Snippets, Components, etc.
npx shadcn-svelte@next add table data-table
select package manager npm Copy Add @tanstack/table-core
as a dependency:
npm install @tanstack/table-core
select package manager npm Copy 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" ,
},
// ...
];
Copy 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
Copy 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.
routes/payments/columns.ts
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" ,
},
];
Copy
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.
routes/payments/data-table.svelte
< 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 DateTableProps < TData , TValue > = {
columns : ColumnDef < TData , TValue >[];
data : TData [];
};
let { data, columns } : DateTableProps < 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 >
Copy
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.
routes/payments/+page.server.ts
export async function load () {
// logic to fetch payments data here
const payments = await getPayments ();
return {
payments,
};
}
Copy routes/payments/+page.svelte
< script lang = "ts" >
import DataTable from "./data-table.svelte" ;
import { columns } from "./columns.js" ;
let { data } = $ props ();
</ script >
< DataTable { data } { columns } />
Copy 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:
routes/payments/columns.ts
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" )))
);
},
},
];
Copy 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.
routes/payments/data-table-actions.svelte
< 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 class = "size-4" />
</ 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 >
Copy Update columns definition Now that we've defined the <DataTableActions />
component, let's update our actions
column definition to use it.
routes/payments/columns.ts
import type { ColumnDef } from "@tanstack/table-core" ;
import { createRawSnippet } from "svelte" ;
import { renderSnippet } 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 });
},
},
];
Copy 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.
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 } : DateTableProps < 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 >
Copy 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.
We can add pagination controls to our table using the <Button />
component and the table.previousPage()
, table.nextPage()
API methods.
routes/payments/data-table.svelte
< script lang = "ts" generics = " TData , TValue " >
import { Button } from "$lib/components/ui/button/index.js" ;
let { columns, data } : DateTableProps < 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 >
Copy See Reusable Components section for a more advanced pagination component.
Sorting Let's make the email column sortable.
We'll start by creating a component to render a sortable email header button.
routes/payments/data-table-email-button.svelte
< 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 size-4" />
</ Button >
Copy Update <DataTable />
routes/payments/data-table.svelte
< script lang = "ts" generics = " TData , TValue " >
import {
type ColumnDef,
type PaginationState,
type SortingState,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
} from "@tanstack/table-core" ;
let { columns, data } : DateTableProps < 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 >
Copy We can now update the email
header cell to add sorting controls.
src/routes/payments/columns.ts
import type { ColumnDef } from "@tanstack/table-core" ;
import {
renderComponent,
renderSnippet,
} 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" ),
}),
},
];
Copy 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 />
routes/payments/data-table.svelte
< 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 } : DateTableProps < 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 >
Copy 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 />
routes/payments/data-table.svelte
< 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 } : DateTableProps < 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"
controlledChecked
checked ={column. getIsVisible ()}
onCheckedChange ={( value ) => column. toggleVisibility ( !! value)}
>
{column.id}
</ DropdownMenu . CheckboxItem >
{/ each }
</ DropdownMenu . Content >
</ DropdownMenu . Root >
</ div >
< div class = "rounded-md border" >
< Table . Root > <!--...--> </ Table . Root >
</ div >
</ div >
Copy 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.
routes/payments/data-table-checkbox.svelte
< script lang = "ts" >
import type { ComponentProps } from "svelte" ;
import { Checkbox } from "$lib/registry/default/ui/checkbox/index.js" ;
let {
checked = false ,
controlledChecked = true ,
... restProps
} : ComponentProps < typeof Checkbox> = $ props ();
</ script >
< Checkbox { checked } { controlledChecked } { ... restProps } />
Copy Update columns definition Now that we have a new component, we can add a select
column definition to render a checkbox.
routes/payments/columns.ts
import type { ColumnDef } from "@tanstack/table-core" ;
import {
renderSnippet,
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 () ||
(table. getIsSomePageRowsSelected () && "indeterminate" ),
onCheckedChange : ( value ) => table. toggleAllPageRowsSelected ( !! value),
controlledChecked: true ,
"aria-label" : "Select all" ,
}),
cell : ({ row }) =>
renderComponent (Checkbox, {
checked: row. getIsSelected (),
onCheckedChange : ( value ) => row. toggleSelected ( !! value),
controlledChecked: true ,
"aria-label" : "Select row" ,
}),
enableSorting: false ,
enableHiding: false ,
},
];
Copy Update <DataTable />
routes/payments/data-table.svelte
< 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 } : DateTableProps < 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 >
Copy 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 >
Copy Reusable Components Check out the Tasks example to learn about creating reusable components for your data tables.