Skip to content

Data Table

Terminal window
bosia add data-table

A fully-featured data table with sorting, filtering, and pagination. Automatically installs dependencies: button, input, separator.

<script lang="ts">
import { DataTable } from "$lib/components/ui/data-table";
import type { ColumnDef, TableState } from "$lib/components/ui/data-table";
type User = { name: string; email: string; role: string };
const columns: ColumnDef<User>[] = [
{ id: "name", accessorKey: "name", header: "Name" },
{ id: "email", accessorKey: "email", header: "Email" },
{ id: "role", accessorKey: "role", header: "Role", enableSorting: false },
];
let users: User[] = $state([
{ name: "Alice", email: "alice@example.com", role: "Admin" },
{ name: "Bob", email: "bob@example.com", role: "User" },
]);
function handleStateChange(state: TableState) {
console.log("Table state:", state);
}
</script>
<DataTable
{columns}
data={users}
totalRows={users.length}
pageSize={10}
onStateChange={handleStateChange}
/>
PropTypeDefault
columnsColumnDef<T>[][]
dataT[][]
totalRowsnumber0
pageSizenumber10
filtersFilterDef[][]
loadingbooleanfalse
emptyMessagestring"No results found."
onStateChange(state: TableState) => void
type ColumnDef<T> = {
id: string;
accessorKey?: keyof T & string;
accessorFn?: (row: T) => any;
header: string | ((ctx: HeaderContext<T>) => RenderDescriptor);
cell?: (ctx: CellContext<T>) => RenderDescriptor;
enableSorting?: boolean; // default: true
class?: string; // applied to <td>
headerClass?: string; // applied to <th>
};

Use renderSnippet for inline HTML or renderComponent for Svelte components:

import { createRawSnippet } from "svelte";
import { renderSnippet, renderComponent } from "$lib/components/ui/data-table";
import Actions from "./actions.svelte";
const columns: ColumnDef<User>[] = [
{
id: "name",
accessorKey: "name",
header: "Name",
cell: ({ value }) => {
const s = createRawSnippet(() => ({
render: () => `<span class="font-bold">${value}</span>`,
}));
return renderSnippet(s);
},
},
{
id: "actions",
header: "Actions",
enableSorting: false,
cell: ({ row }) => renderComponent(Actions, { id: row.id }),
},
];
<script lang="ts">
import type { FilterDef } from "$lib/components/ui/data-table";
const filters: FilterDef[] = [
{ id: "search", label: "Search", type: "text", placeholder: "Filter by name..." },
{
id: "role",
label: "Role",
type: "select",
options: [
{ label: "All", value: "" },
{ label: "Admin", value: "admin" },
{ label: "User", value: "user" },
],
},
];
</script>
<DataTable {columns} {filters} data={users} totalRows={users.length} />

The onStateChange callback receives the full table state (sort, filters, pagination), making it easy to drive server-side data fetching:

<script lang="ts">
let data: User[] = $state([]);
let total = $state(0);
let loading = $state(false);
async function handleStateChange(state: TableState) {
loading = true;
const res = await fetch(`/api/users?${new URLSearchParams({
page: String(state.pagination.page),
pageSize: String(state.pagination.pageSize),
sortBy: state.sort?.id ?? "",
sortDesc: String(state.sort?.desc ?? false),
...state.filters,
})}`);
const json = await res.json();
data = json.rows;
total = json.total;
loading = false;
}
</script>
<DataTable
{columns}
{data}
totalRows={total}
{loading}
onStateChange={handleStateChange}
/>