Table Builder
Generate production-ready data tables with sorting, filtering, and pagination.
Core Workflow
-
Define columns: Column configuration with types
-
Choose mode: Server-side or client-side rendering
-
Add features: Sorting, filtering, pagination, search
-
Row actions: Edit, delete, view actions
-
Empty states: No data and error views
-
Loading states: Skeletons and suspense
-
Mobile responsive: Stack columns or horizontal scroll
Column Configuration
import { ColumnDef } from "@tanstack/react-table";
export const columns: ColumnDef<User>[] = [ { accessorKey: "id", header: "ID", cell: ({ row }) => ( <span className="font-mono text-sm">{row.original.id}</span> ), }, { accessorKey: "name", header: ({ column }) => ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Name <ArrowUpDown className="ml-2 h-4 w-4" /> </Button> ), cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "email", header: "Email", }, { accessorKey: "status", header: "Status", cell: ({ row }) => { const status = row.getValue("status") as string; return ( <Badge variant={status === "active" ? "success" : "secondary"}> {status} </Badge> ); }, }, { id: "actions", cell: ({ row }) => <RowActions row={row} />, }, ];
React Table Implementation
"use client";
import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, flexRender, } from "@tanstack/react-table";
export function DataTable<TData, TValue>({ columns, data, }: { columns: ColumnDef<TData, TValue>[]; data: TData[]; }) { const [sorting, setSorting] = useState<SortingState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onPaginationChange: setPagination, state: { sorting, columnFilters, pagination }, });
return ( <div className="space-y-4"> <div className="flex items-center justify-between"> <Input placeholder="Search..." value={(table.getColumn("name")?.getFilterValue() as string) ?? ""} onChange={(e) => table.getColumn("name")?.setFilterValue(e.target.value) } className="max-w-sm" /> </div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<DataTablePagination table={table} />
</div>
); }
Server-Side Pattern
// app/users/page.tsx import { DataTable } from "@/components/ui/data-table";
async function getUsers(params: {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: "asc" | "desc";
search?: string;
}) {
const response = await fetch(/api/users?${new URLSearchParams(params)});
return response.json();
}
export default async function UsersPage({ searchParams, }: { searchParams: { page?: string; search?: string }; }) { const page = Number(searchParams.page) || 1; const search = searchParams.search || "";
const { data, totalPages } = await getUsers({ page, pageSize: 10, search });
return ( <div className="space-y-4"> <h1 className="text-3xl font-bold">Users</h1> <DataTable columns={columns} data={data} totalPages={totalPages} /> </div> ); }
Pagination Component
export function DataTablePagination({ table }: { table: Table<any> }) {
return (
<div className="flex items-center justify-between px-2">
<div className="text-sm text-gray-700">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected
</div>
<div className="flex items-center space-x-6">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={${table.getState().pagination.pageSize}}
onValueChange={(value) => table.setPageSize(Number(value))}
>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
</Select>
</div>
<div className="flex items-center space-x-2">
<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>
</div>
);
}
Row Actions
function RowActions({ row }: { row: Row<User> }) { const user = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(user.id)}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push(/users/${user.id})}>
View Details
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(user)}>
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => handleDelete(user)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Empty State
export function EmptyState() { return ( <div className="flex flex-col items-center justify-center py-12"> <div className="rounded-full bg-gray-100 p-6"> <InboxIcon className="h-12 w-12 text-gray-400" /> </div> <h3 className="mt-4 text-lg font-semibold">No data found</h3> <p className="mt-2 text-sm text-gray-600"> Get started by creating a new record. </p> <Button className="mt-4">Create New</Button> </div> ); }
Loading Skeleton
export function TableSkeleton() { return ( <div className="space-y-4"> <Skeleton className="h-10 w-64" /> <div className="rounded-md border"> {Array.from({ length: 10 }).map((_, i) => ( <div key={i} className="flex gap-4 border-b p-4"> <Skeleton className="h-6 w-full" /> </div> ))} </div> </div> ); }
Best Practices
-
Use TanStack Table for complex features
-
Server-side pagination for large datasets
-
Debounce search inputs
-
Persist sorting/filtering in URL params
-
Mobile: Horizontal scroll or card view
-
Accessibility: Keyboard navigation, ARIA
Output Checklist
-
Column definitions with types
-
Sorting functionality
-
Filtering/search
-
Pagination controls
-
Row actions menu
-
Empty state component
-
Loading skeleton
-
Mobile responsive
-
URL state persistence
-
Accessibility attributes