Data tables
Production-grade SaaS UI skills for AI coding agents (React + Tailwind + shadcn/ui) — Codex, Claude Code, Cursor, OpenCode. One npx command to install.
npx -y skills add param087/saas-ui-skills --skill data-tablesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when building data tables — sorting, filtering, pagination, row selection and bulk actions with TanStack Table + shadcn/ui, plus the loading, empty, and error states tables almost always forget.
SKILL.md
4.1 KB, as published. Nobody here has run it
Data Tables
Overview
Tables are where SaaS apps live (users, invoices, logs). Build them with TanStack Table (headless logic: sorting, filtering, pagination, selection) rendered through shadcn/ui Table primitives. Separate column definitions from the table shell so every table in the app behaves consistently.
When to use
- Displaying lists of records with sort/filter/paginate.
- Bulk actions on selected rows.
- A
<table>is being hand-built with manual sort state.
Column definitions
import { ColumnDef } from "@tanstack/react-table";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
export type Invoice = { id: string; customer: string; status: "paid" | "open" | "void"; amount: number };
export const columns: ColumnDef<Invoice>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(v) => row.toggleSelected(!!v)}
aria-label="Select row"
/>
),
enableSorting: false,
},
{ accessorKey: "customer", header: "Customer" },
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => <Badge variant={row.original.status === "paid" ? "success" : "secondary"}>{row.original.status}</Badge>,
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => <div className="text-right tabular-nums">${(row.original.amount / 100).toFixed(2)}</div>,
},
];
The reusable shell
const table = useReactTable({
data, columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: { sorting, rowSelection, globalFilter },
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection,
});
Render header/body from table.getHeaderGroups() / table.getRowModel().rows. Always handle the three non-happy states:
{isLoading ? (
<TableRow><TableCell colSpan={columns.length}><Skeleton className="h-8 w-full" /></TableCell></TableRow>
) : rows.length === 0 ? (
<TableRow><TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground">No results.</TableCell></TableRow>
) : (
rows.map((row) => <TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>…</TableRow>
)}
Bulk actions
When table.getSelectedRowModel().rows.length > 0, show an action bar:
{selectedCount > 0 && (
<div className="flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-sm">
<span>{selectedCount} selected</span>
<Button size="sm" variant="destructive" onClick={deleteSelected}>Delete</Button>
</div>
)}
Server-side at scale
For large datasets, set manualPagination/manualSorting/manualFiltering: true and pass state to your API. Keep filter/sort/page in the URL (searchParams) so views are shareable and survive refresh.
Pitfalls
- No loading/empty/error states — the #1 omission; a blank table looks broken.
- Right-align numbers +
tabular-nums— left-aligned currency is hard to scan. - Client-paginating 50k rows — move to server-side; don't ship it all.
- Selection lost on page change unless you key it by row id.
- Non-accessible sort headers — make them
<button>s witharia-sort. - Filter/sort only in component state — put it in the URL for shareable views.
Hand-off
A consistent table that pairs with empty-and-loading-states (skeletons), modals-and-dialogs (row edit/confirm), and notifications-and-toasts (bulk-action feedback).