Data Table

Data Display

Feature-rich table with sorting and selection.

Preview

Project AlphaActive$2,500
Project BetaPending$1,200
Project GammaComplete$4,800
Project DeltaActive$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