Accessile Reusable Data Table Components for Next.js – Table-Forge

A collection of beautiful data table components with sorting, filtering, and pagination features. Built with Next.js, Tailwind CSS, and shadcn/ui.

Table-Forge is a collection of beautiful, accessible, reusable data table components for modern next.js-powered web applications.

Each component comes with full source code that you can copy and modify for your specific use cases.

Features

📋 Ready-to-use table components with minimal setup required.

🎨 Built with Tailwind CSS and shadcn/ui for consistent utility-first styling.

♿ Accessible table structure following WCAG guidelines

🔄 Sortable and filterable tables.

Basic Data Table

export default function MinimalistUI() {
return (
<div className="rounded-lg border overflow-hidden">
<table className="w-full caption-bottom text-sm">
<thead className="[&_tr]:border-b">
<tr className="border-b">
<th className="text-muted-foreground h-10 px-4 text-left align-middle font-medium">
Name
</th>
<th className="text-muted-foreground h-10 px-4 text-left align-middle font-medium">
Title
</th>
<th className="text-muted-foreground h-10 px-4 text-left align-middle font-medium">
Email
</th>
<th className="text-muted-foreground h-10 px-4 text-left align-middle font-medium">
Role
</th>
</tr>
</thead>
<tbody className="[&_tr:last-child]:border-0">
<tr className="hover:bg-muted/50 border-b transition-colors">
<td className="p-4 align-middle">Lindsay Walton</td>
<td className="p-4 align-middle">Front-end Developer</td>
<td className="p-4 align-middle">[email protected]</td>
<td className="p-4 align-middle">Member</td>
</tr>
<tr className="hover:bg-muted/50 border-b transition-colors">
<td className="p-4 align-middle">Courtney Henry</td>
<td className="p-4 align-middle">Designer</td>
<td className="p-4 align-middle">[email protected]</td>
<td className="p-4 align-middle">Admin</td>
</tr>
<tr className="hover:bg-muted/50 border-b transition-colors">
<td className="p-4 align-middle">Tom Cook</td>
<td className="p-4 align-middle">Director of Product</td>
<td className="p-4 align-middle">[email protected]</td>
<td className="p-4 align-middle">Owner</td>
</tr>
</tbody>
</table>
</div>
);
}

Sortable Table

"use client";

import { useMemo, useState } from "react";

type Person = {
name: string;
title: string;
email: string;
role: string;
};

type SortKey = keyof Person;
type SortDirection = "asc" | "desc";

const initialData: Person[] = [
{
name: "Lindsay Walton",
title: "Front-end Developer",
email: "[email protected]",
role: "Member",
},
{
name: "Courtney Henry",
title: "Designer",
email: "[email protected]",
role: "Admin",
},
{
name: "Tom Cook",
title: "Director of Product",
email: "[email protected]",
role: "Owner",
},
{
name: "Jacob Jones",
title: "Engineering Manager",
email: "[email protected]",
role: "Member",
},
];

export default function SortableTable() {
const [sortKey, setSortKey] = useState<SortKey>("name");
const [sortDirection, setSortDirection] = useState<SortDirection>("asc");

function onHeaderClick(key: SortKey) {
if (key === sortKey) {
setSortDirection((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
setSortDirection("asc");
}
}

const sorted = useMemo(() => {
const data = [...initialData];
data.sort((a, b) => {
const aVal = String(a[sortKey]).toLowerCase();
const bVal = String(b[sortKey]).toLowerCase();
if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
return 0;
});
return data;
}, [sortKey, sortDirection]);

function renderSortIcon(key: SortKey) {
if (key !== sortKey)
return (
<span
aria-hidden
className="ml-1 select-none text-muted-foreground"
></span>
);
return (
<span aria-hidden className="ml-1 select-none text-foreground">
{sortDirection === "asc" ? "↑" : "↓"}
</span>
);
}

return (
<table className="min-w-full divide-y divide-border text-left text-sm">
<thead className="bg-muted text-muted-foreground">
<tr>
<th className="px-4 py-2 font-semibold">
<button
type="button"
onClick={() => onHeaderClick("name")}
className="inline-flex items-center group"
aria-sort={
sortKey === "name"
? sortDirection === "asc"
? "ascending"
: "descending"
: "none"
}
>
<span className="group-hover:underline">Name</span>{" "}
{renderSortIcon("name")}
</button>
</th>
<th className="px-4 py-2 font-semibold">
<button
type="button"
onClick={() => onHeaderClick("title")}
className="inline-flex items-center group"
aria-sort={
sortKey === "title"
? sortDirection === "asc"
? "ascending"
: "descending"
: "none"
}
>
<span className="group-hover:underline">Title</span>{" "}
{renderSortIcon("title")}
</button>
</th>
<th className="px-4 py-2 font-semibold">
<button
type="button"
onClick={() => onHeaderClick("email")}
className="inline-flex items-center group"
aria-sort={
sortKey === "email"
? sortDirection === "asc"
? "ascending"
: "descending"
: "none"
}
>
<span className="group-hover:underline">Email</span>{" "}
{renderSortIcon("email")}
</button>
</th>
<th className="px-4 py-2 font-semibold">
<button
type="button"
onClick={() => onHeaderClick("role")}
className="inline-flex items-center group"
aria-sort={
sortKey === "role"
? sortDirection === "asc"
? "ascending"
: "descending"
: "none"
}
>
<span className="group-hover:underline"> Role</span>{" "}
{renderSortIcon("role")}
</button>
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{sorted.map((p) => (
<tr key={p.email}>
<td className="px-4 py-2">{p.name}</td>
<td className="px-4 py-2">{p.title}</td>
<td className="px-4 py-2">{p.email}</td>
<td className="px-4 py-2">{p.role}</td>
</tr>
))}
</tbody>
</table>
);
}

Filterable Table

"use client";

import { useMemo, useState } from "react";

type Person = {
name: string;
title: string;
email: string;
role: string;
};

const initialData: Person[] = [
{
name: "Lindsay Walton",
title: "Front-end Developer",
email: "[email protected]",
role: "Member",
},
{
name: "Courtney Henry",
title: "Designer",
email: "[email protected]",
role: "Admin",
},
{
name: "Tom Cook",
title: "Director of Product",
email: "[email protected]",
role: "Owner",
},
{
name: "Jacob Jones",
title: "Engineering Manager",
email: "[email protected]",
role: "Member",
},
];

export default function FilterableTable() {
const [query, setQuery] = useState("");

const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return initialData;
return initialData.filter((p) =>
[p.name, p.title, p.email, p.role].some((v) =>
v.toLowerCase().includes(q)
)
);
}, [query]);

return (
<div className="w-full">
<div className="mb-2">
<label htmlFor="filter" className="sr-only">
Search
</label>
<input
id="filter"
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search name, title, email, role..."
className="w-full rounded-md border px-3 py-2 text-sm placeholder:text-muted-foreground outline-none"
/>
</div>

<table className="min-w-full divide-y divide-border text-left text-sm">
<thead className="bg-muted text-muted-foreground">
<tr>
<th className="px-4 py-2 font-semibold">Name</th>
<th className="px-4 py-2 font-semibold">Title</th>
<th className="px-4 py-2 font-semibold">Email</th>
<th className="px-4 py-2 font-semibold">Role</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{filtered.map((p) => (
<tr key={p.email}>
<td className="px-4 py-2">{p.name}</td>
<td className="px-4 py-2">{p.title}</td>
<td className="px-4 py-2">{p.email}</td>
<td className="px-4 py-2">{p.role}</td>
</tr>
))}
{filtered.length === 0 && (
<tr>
<td
className="px-4 py-6 text-center text-sm text-muted-foreground"
colSpan={4}
>
No results
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}

With Pagination Controls

"use client";

import { useEffect, useMemo, useState } from "react";

type Person = {
name: string;
title: string;
email: string;
role: string;
};

const sampleData: Person[] = [
{
name: "Lindsay Walton",
title: "Front-end Developer",
email: "[email protected]",
role: "Member",
},
{
name: "Courtney Henry",
title: "Designer",
email: "[email protected]",
role: "Admin",
},
{
name: "Tom Cook",
title: "Director of Product",
email: "[email protected]",
role: "Owner",
},
{
name: "Jacob Jones",
title: "Engineering Manager",
email: "[email protected]",
role: "Member",
},
{
name: "Esther Howard",
title: "Account Manager",
email: "[email protected]",
role: "Member",
},
{
name: "Cody Fisher",
title: "Back-end Developer",
email: "[email protected]",
role: "Admin",
},
//... and so on
];

// Create 42 entries
const initialData: Person[] = Array.from({ length: 42 }, (_, index) => {
const base = sampleData[index % sampleData.length];
const nameSlug = base.name.toLowerCase().replace(/\s+/g, "");
return {
...base,
email: `${nameSlug}${index}@example.com`,
};
});

export default function PaginatedTable() {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);

const totalItems = initialData.length;
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));

useEffect(() => {
setCurrentPage((p) => Math.min(p, totalPages));
}, [pageSize, totalPages]);

const pageData = useMemo(() => {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
return initialData.slice(start, end);
}, [currentPage, pageSize]);

const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, totalItems);

function goToFirst() {
setCurrentPage(1);
}
function goToPrev() {
setCurrentPage((p) => Math.max(1, p - 1));
}
function goToNext() {
setCurrentPage((p) => Math.min(totalPages, p + 1));
}
function goToLast() {
setCurrentPage(totalPages);
}

return (
<div className="w-full">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-muted-foreground">
Rows per page
</span>
<select
aria-label="Rows per page"
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="rounded-md border px-2 py-1 text-xs bg-background text-foreground"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
</select>
</div>

<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">
Showing {startItem}–{endItem} of {totalItems}
</span>
<div className="ml-2 h-4 w-px bg-border" />
<div className="flex items-center gap-1">
<button
type="button"
onClick={goToFirst}
disabled={currentPage === 1}
className="rounded-md border px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted"
aria-label="Go to first page"
>
« First
</button>
<button
type="button"
onClick={goToPrev}
disabled={currentPage === 1}
className="rounded-md border px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted"
aria-label="Go to previous page"
>
‹ Prev
</button>
<span className="px-2 text-muted-foreground">
Page {currentPage} of {totalPages}
</span>
<button
type="button"
onClick={goToNext}
disabled={currentPage === totalPages}
className="rounded-md border px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted"
aria-label="Go to next page"
>
Next ›
</button>
<button
type="button"
onClick={goToLast}
disabled={currentPage === totalPages}
className="rounded-md border px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted"
aria-label="Go to last page"
>
Last »
</button>
</div>
</div>
</div>

<table className="min-w-full divide-y divide-border text-left text-sm">
<thead className="bg-muted text-muted-foreground">
<tr>
<th className="px-4 py-2 font-semibold">Name</th>
<th className="px-4 py-2 font-semibold">Title</th>
<th className="px-4 py-2 font-semibold">Email</th>
<th className="px-4 py-2 font-semibold">Role</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{pageData.map((p) => (
<tr key={p.email}>
<td className="px-4 py-2">{p.name}</td>
<td className="px-4 py-2">{p.title}</td>
<td className="px-4 py-2">{p.email}</td>
<td className="px-4 py-2">{p.role}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

Related Resources

  • Tailwind CSS – Utility-first CSS framework used for styling all table components
  • Shadcn/ui – Component library that provides additional UI elements for enhanced table functionality
  • TanStack Table – Headless table library for building complex data tables with advanced features

FAQs

Q: Is Table-Forge a dependency I need to install?
A: No, Table-Forge is not a package to install. It is a collection of code snippets that you can copy and paste directly into your project.

Q: Can I customize the appearance of the tables?
A: Yes. Since the components are built with Tailwind CSS, you can modify the utility classes directly in the JSX to match your project’s design system.

Q: Are the tables compatible with server-side rendering?
A: The components work with both client-side and server-side rendering. They support Next.js 15 app router and server components.

Anmoldeep Singh

Anmoldeep Singh

Software engineer passionate about building web applications and crafting meaningful digital experiences.

Leave a Reply

Your email address will not be published. Required fields are marked *