Data Table
Data DisplayFeature-rich table with sorting and selection.
Preview
| Project Alpha | Active | $2,500 |
| Project Beta | Pending | $1,200 |
| Project Gamma | Complete | $4,800 |
| Project Delta | Active | $900 |
Usage
example.jsx
import { DataTable } from "@/components/ui/data-table";
export default function Example() {
return <DataTable />;
}Source Code
Copy this file into components/ui/data-table.jsx in your project.
data-table.jsx
"use client";
import { forwardRef, useState, useMemo } from "react";
import { cn } from "@/lib/utils";
const DataTable = forwardRef(({ className, columns = [], data = [], pageSize = 10, searchable, ...props }, ref) => {
const [page, setPage] = useState(0);
const [sortKey, setSortKey] = useState(null);
const [sortDir, setSortDir] = useState("asc");
const [search, setSearch] = useState("");
const filtered = useMemo(() => {
if (!search) return data;
return data.filter((row) => columns.some((col) => String(row[col.key] || "").toLowerCase().includes(search.toLowerCase())));
}, [data, search, columns]);
const sorted = useMemo(() => {
if (!sortKey) return filtered;
return [...filtered].sort((a, b) => {
const av = a[sortKey], bv = b[sortKey];
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
return sortDir === "asc" ? cmp : -cmp;
});
}, [filtered, sortKey, sortDir]);
const paged = sorted.slice(page * pageSize, (page + 1) * pageSize);
const totalPages = Math.ceil(sorted.length / pageSize);
const toggleSort = (key) => {
if (sortKey === key) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
else { setSortKey(key); setSortDir("asc"); }
};
return (
<div ref={ref} className={cn("w-full space-y-4", className)} {...props}>
{searchable && (
<input placeholder="Search…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
className="h-9 w-full max-w-sm rounded-md border bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
/>
)}
<div className="overflow-auto rounded-md border">
<table className="w-full caption-bottom text-sm">
<thead className="[&_tr]:border-b">
<tr>
{columns.map((col) => (
<th key={col.key} onClick={() => col.sortable !== false && toggleSort(col.key)}
className={cn("h-10 px-4 text-left align-middle font-medium text-muted-foreground", col.sortable !== false && "cursor-pointer select-none hover:text-foreground")}
>
{col.label}{sortKey === col.key && (sortDir === "asc" ? " ↑" : " ↓")}
</th>
))}
</tr>
</thead>
<tbody>
{paged.map((row, i) => (
<tr key={i} className="border-b transition-colors hover:bg-muted/50">
{columns.map((col) => (
<td key={col.key} className="p-4 align-middle">{col.render ? col.render(row[col.key], row) : row[col.key]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>{sorted.length} row(s)</span>
<div className="flex gap-1">
<button disabled={page === 0} onClick={() => setPage(page - 1)} className="rounded border px-3 py-1 hover:bg-accent disabled:opacity-50">Prev</button>
<span className="px-3 py-1">{page + 1} / {totalPages}</span>
<button disabled={page >= totalPages - 1} onClick={() => setPage(page + 1)} className="rounded border px-3 py-1 hover:bg-accent disabled:opacity-50">Next</button>
</div>
</div>
)}
</div>
);
});
DataTable.displayName = "DataTable";
export { DataTable };
Quick Install
Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.
npm install clsx tailwind-merge