- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Menubar
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
- Typography
Installation#
pnpm dlx shadcn@latest add @force-ui-svelte/data-table
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.sveltecolumns.tswill contain our column definitions.data-table.sveltewill contain the<Table />component and the complete<DataTable />component.data-table-actions.sveltewill contain the actions menu for each row.data-table-checkbox.sveltewill contain the checkbox for each row.data-table-email-button.sveltewill contain the sortable email header button.+page.svelteis 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",
},
]<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 colspan={header.colSpan}>
{#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>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={data.payments} {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 { renderSnippet } from "$lib/components/ui/data-table/index.js"
import { createRawSnippet } from "svelte"
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "amount",
header: () => {
const amountHeaderSnippet = createRawSnippet(() => ({
render: () => `<div class="text-end">Amount</div>`,
}))
return renderSnippet(amountHeaderSnippet)
},
cell: ({ row }) => {
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
})
const amountCellSnippet = createRawSnippet<[{ amount: number }]>(
(getAmount) => {
const { amount } = getAmount()
const formatted = formatter.format(amount)
return {
render: () =>
`<div class="text-end font-medium">${formatted}</div>`,
}
}
)
return renderSnippet(amountCellSnippet, {
amount: row.original.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 /> and the <Button /> components for this, so you have install them if not done already:
Create actions component#
We'll start by defining the actions menu in our data-table-actions.svelte component.
<script lang="ts">
import EllipsisIcon 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>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<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,
state: {
get pagination() {
return pagination;
},
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
</script>
<div>
<div class="rounded-md border">
<Table.Root>
</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 ArrowUpDownIcon 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
<ArrowUpDownIcon class="ms-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.getToggleSortingHandler(),
}),
},
]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>
</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 { Checkbox } from "$lib/components/ui/checkbox/index.js"
import { renderComponent } from "$lib/components/ui/data-table/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
InstallationPrerequisitesProject StructureBasic TableColumn Definitions<DataTable /> ComponentRender the tableCell FormattingUpdate columns definitionRow ActionsCreate actions componentUpdate columns definitionPaginationUpdate <DataTable />Adding pagination controlsSortingDefine <DataTableEmailButton /> componentUpdate <DataTable />Make header cell sortableFilteringUpdate <DataTable />VisibilityUpdate <DataTable />Row SelectionDefine <DataTableCheckbox /> componentUpdate columns definitionUpdate <DataTable />Show selected rowsReusable Components