Saasflare UI
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.json
pnpm add @saasflare/ui

Usage

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

On this page