Components
Data Table
Dependency-free data grid built on the Table primitives — typed columns, client-side sort, row selection, and pagination out of the box, with a server-side/TanStack escape hatch.
Installation
npx shadcn@latest add https://ui.saasflare.io/r/data-table.jsonpnpm add @saasflare/uiUsage
Loading…
import { DataTable, Badge, type DataTableColumn } from "@saasflare/ui"interface Member { id: string name: string email: string role: "Owner" | "Admin" | "Member" status: "active" | "invited"}const members: Member[] = [ { id: "u1", name: "Ada Lovelace", email: "ada@saasflare.io", role: "Owner", status: "active" }, { id: "u2", name: "Grace Hopper", email: "grace@saasflare.io", role: "Admin", status: "active" }, { id: "u3", name: "Alan Turing", email: "alan@saasflare.io", role: "Member", status: "invited" }, { id: "u4", name: "Katherine Johnson", email: "kat@saasflare.io", role: "Member", status: "active" },]const columns: DataTableColumn<Member>[] = [ { accessorKey: "name", header: "Name" }, { accessorKey: "email", header: "Email" }, { accessorKey: "role", header: "Role" }, { accessorKey: "status", header: "Status", align: "end", cell: (row) => ( <Badge intent={row.status === "active" ? "success" : "neutral"} variant="soft"> {row.status === "active" ? "Active" : "Invited"} </Badge> ), },]/** Zero-config table: typed columns + data, no wiring required. */export function Demo() { return <DataTable data={members} columns={columns} getRowId="id" />}Selection
Loading…
"use client"import { useState } from "react"import { DataTable, Badge, Button, XIcon, type DataTableColumn } from "@saasflare/ui"interface Invoice { id: string number: string customer: string amount: string status: "paid" | "open" | "overdue"}const invoices: Invoice[] = [ { id: "inv-1041", number: "#1041", customer: "Northwind Co.", amount: "$1,200.00", status: "paid" }, { id: "inv-1042", number: "#1042", customer: "Acme Inc.", amount: "$840.00", status: "open" }, { id: "inv-1043", number: "#1043", customer: "Globex", amount: "$3,150.00", status: "overdue" }, { id: "inv-1044", number: "#1044", customer: "Initech", amount: "$420.00", status: "paid" }, { id: "inv-1045", number: "#1045", customer: "Soylent", amount: "$2,000.00", status: "open" },]const statusIntent = { paid: "success", open: "info", overdue: "danger" } as constconst columns: DataTableColumn<Invoice>[] = [ { accessorKey: "number", header: "Invoice" }, { accessorKey: "customer", header: "Customer" }, { accessorKey: "amount", header: "Amount", align: "end" }, { accessorKey: "status", header: "Status", align: "end", cell: (row) => ( <Badge intent={statusIntent[row.status]} variant="soft"> {row.status} </Badge> ), },]/** Controlled multi-selection driving a bulk-action toolbar. */export function Demo() { const [selectedKeys, setSelectedKeys] = useState<string[]>(["inv-1042"]) return ( <div className="space-y-3"> {selectedKeys.length > 0 ? ( <div className="flex items-center justify-between rounded-lg border bg-muted/40 px-3 py-2"> <span className="text-sm text-muted-foreground"> {selectedKeys.length} selected </span> <div className="flex items-center gap-2"> <Button size="sm" variant="soft"> Send reminder </Button> <Button size="sm" variant="ghost" onClick={() => setSelectedKeys([])}> <XIcon /> Clear </Button> </div> </div> ) : null} <DataTable data={invoices} columns={columns} getRowId="id" selectionMode="multiple" selectedKeys={selectedKeys} onSelectedKeysChange={setSelectedKeys} /> </div> )}Server Tanstack
Loading…
"use client"import { useEffect, useState } from "react"import { DataTable, type DataTableColumn, type DataTableSort } from "@saasflare/ui"interface Repo { id: string name: string stars: number language: string}// Pretend this lives behind an API. The "server" owns sort + the page window;// the table never sorts or slices locally (manualSort + manualPagination).const ALL: Repo[] = [ { id: "r1", name: "saasflare/ui", stars: 4820, language: "TypeScript" }, { id: "r2", name: "saasflare/cli", stars: 1190, language: "Go" }, { id: "r3", name: "saasflare/edge", stars: 932, language: "Rust" }, { id: "r4", name: "saasflare/docs", stars: 612, language: "MDX" }, { id: "r5", name: "saasflare/sdk-py", stars: 2304, language: "Python" }, { id: "r6", name: "saasflare/sdk-js", stars: 3870, language: "TypeScript" }, { id: "r7", name: "saasflare/charts", stars: 740, language: "TypeScript" }, { id: "r8", name: "saasflare/auth", stars: 1560, language: "Go" },]const PAGE_SIZE = 3/** Simulates a paginated, server-sorted endpoint. */function fetchRepos(sort: DataTableSort[], page: number): Promise<{ rows: Repo[]; total: number }> { const sorted = [...ALL] const s = sort[0] if (s) { const key = s.id as keyof Repo sorted.sort((a, b) => { const cmp = a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0 return s.desc ? -cmp : cmp }) } const start = (page - 1) * PAGE_SIZE return new Promise((resolve) => setTimeout(() => resolve({ rows: sorted.slice(start, start + PAGE_SIZE), total: ALL.length }), 350), )}const columns: DataTableColumn<Repo>[] = [ { accessorKey: "name", header: "Repository", sortable: true }, { accessorKey: "language", header: "Language", sortable: true }, { accessorKey: "stars", header: "Stars", sortable: true, align: "end", cell: (r) => r.stars.toLocaleString() },]/** * Escape hatch: server-driven sort + pagination via `manualSort` + * `manualPagination`. `onSortChange` / `onPageChange` re-fetch the window; the * component stays dependency-free. * * ───────────────────────────────────────────────────────────────────────── * TanStack adapter (illustrative — `@tanstack/react-table` is NEVER a * dependency of `@saasflare/ui`; you'd add it yourself in your app): * * import { useReactTable, getCoreRowModel } from "@tanstack/react-table" * * const tt = useReactTable({ * data: rows, * columns: tanstackColumns, * manualSorting: true, * manualPagination: true, * state: { sorting, pagination }, * onSortingChange: setSorting, // → maps to DataTable's onSortChange * onPaginationChange: setPagination, * getCoreRowModel: getCoreRowModel(), * }) * // then feed tt.getRowModel().rows into <DataTable data=… manualSort manualPagination /> * ───────────────────────────────────────────────────────────────────────── */export function Demo() { const [sort, setSort] = useState<DataTableSort[]>([{ id: "stars", desc: true }]) const [page, setPage] = useState(1) const [rows, setRows] = useState<Repo[]>([]) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) useEffect(() => { let active = true setLoading(true) fetchRepos(sort, page).then((res) => { if (!active) return setRows(res.rows) setTotal(res.total) setLoading(false) }) return () => { active = false } }, [sort, page]) return ( <DataTable data={rows} columns={columns} getRowId="id" loading={loading} // Server owns ordering + the page window: manualSort sort={sort} onSortChange={(next) => { setSort(next) setPage(1) }} manualPagination rowCount={total} pageSize={PAGE_SIZE} page={page} onPageChange={setPage} /> )}Sorting Pagination
Loading…
"use client"import { useState } from "react"import { DataTable, Label, Switch, type DataTableColumn, type DataTableDensity } from "@saasflare/ui"interface Deployment { id: string service: string env: "production" | "staging" | "preview" duration: number deployedAt: string}const deployments: Deployment[] = [ { id: "d1", service: "api-gateway", env: "production", duration: 142, deployedAt: "2026-05-28" }, { id: "d2", service: "web", env: "production", duration: 89, deployedAt: "2026-05-28" }, { id: "d3", service: "auth", env: "staging", duration: 61, deployedAt: "2026-05-27" }, { id: "d4", service: "billing", env: "production", duration: 203, deployedAt: "2026-05-27" }, { id: "d5", service: "worker", env: "preview", duration: 47, deployedAt: "2026-05-26" }, { id: "d6", service: "search", env: "staging", duration: 118, deployedAt: "2026-05-26" }, { id: "d7", service: "notifications", env: "preview", duration: 33, deployedAt: "2026-05-25" }, { id: "d8", service: "analytics", env: "production", duration: 176, deployedAt: "2026-05-25" }, { id: "d9", service: "cron", env: "staging", duration: 52, deployedAt: "2026-05-24" }, { id: "d10", service: "media", env: "preview", duration: 95, deployedAt: "2026-05-24" },]const columns: DataTableColumn<Deployment>[] = [ { accessorKey: "service", header: "Service", sortable: true }, { accessorKey: "env", header: "Environment", sortable: true }, { accessorKey: "duration", header: "Build (s)", sortable: true, align: "end", cell: (row) => `${row.duration}s`, }, { accessorKey: "deployedAt", header: "Deployed", sortable: true, align: "end" },]/** Sortable headers (shift-click for multi-sort), paginated 5/page, with a density toggle. */export function Demo() { const [density, setDensity] = useState<DataTableDensity>("comfortable") return ( <div className="space-y-3"> <div className="flex items-center justify-end gap-2"> <Switch id="density" checked={density === "compact"} onCheckedChange={(on) => setDensity(on ? "compact" : "comfortable")} /> <Label htmlFor="density">Compact rows</Label> </div> <DataTable data={deployments} columns={columns} getRowId="id" multiSort defaultSort={[{ id: "deployedAt", desc: true }]} pageSize={5} density={density} /> </div> )}States
Loading…
"use client"import { useState } from "react"import { DataTable, Button, MagnifyingGlassIcon, type DataTableColumn } from "@saasflare/ui"interface Event { id: string type: string actor: string at: string}const events: Event[] = [ { id: "e1", type: "user.login", actor: "ada@saasflare.io", at: "12:04:21" }, { id: "e2", type: "billing.charge", actor: "system", at: "12:03:58" }, { id: "e3", type: "project.create", actor: "grace@saasflare.io", at: "12:01:10" }, { id: "e4", type: "key.rotate", actor: "alan@saasflare.io", at: "11:58:42" }, { id: "e5", type: "webhook.fail", actor: "system", at: "11:55:03" }, { id: "e6", type: "user.invite", actor: "ada@saasflare.io", at: "11:50:17" }, { id: "e7", type: "member.remove", actor: "grace@saasflare.io", at: "11:47:29" },]const columns: DataTableColumn<Event>[] = [ { accessorKey: "type", header: "Event" }, { accessorKey: "actor", header: "Actor" }, { accessorKey: "at", header: "Time", align: "end" },]type View = "data" | "loading" | "empty"/** Loading skeletons, an empty state, and a sticky header inside a scroll container. */export function Demo() { const [view, setView] = useState<View>("data") const rows = view === "empty" ? [] : events return ( <div className="space-y-3"> <div className="flex flex-wrap gap-2"> <Button size="sm" variant={view === "data" ? "solid" : "soft"} onClick={() => setView("data")}> Data </Button> <Button size="sm" variant={view === "loading" ? "solid" : "soft"} onClick={() => setView("loading")}> Loading </Button> <Button size="sm" variant={view === "empty" ? "solid" : "soft"} onClick={() => setView("empty")}> Empty </Button> </div> <DataTable data={rows} columns={columns} getRowId="id" loading={view === "loading"} loadingRows={5} stickyHeader maxHeight="16rem" emptyState={ <div className="flex flex-col items-center gap-2 py-6 text-muted-foreground"> <MagnifyingGlassIcon className="size-6" /> <span>No events in this window.</span> </div> } /> </div> )}API Reference
Prop
Type