A highly customizable and reusable table component for Next.js applications, built with TypeScript and TailwindCSS.
npm install nextjs-reusable-table
# or
yarn add nextjs-reusable-table
# or
pnpm add nextjs-reusable-table
- Next.js 12 or later
- React 16 or later
- React DOM 16 or later
- Tailwind CSS 3.0 or later
- TypeScript (recommended)
"use client";
import React from "react";
import { TableComponent } from "nextjs-reusable-table";
import "nextjs-reusable-table/dist/index.css";
interface User {
id: number;
name: string;
email: string;
balance: string;
status: string;
createdAt: string;
tags: string[];
}
const MyTable = () => {
const data: User[] = [
{
id: 1,
name: "Alice Johnson",
email: "[email protected]",
balance: "1200.45",
status: "active",
createdAt: "2024-01-15T10:30:00Z",
tags: ["VIP", "Early Adopter"],
},
{
id: 2,
name: "Bob Smith",
email: "[email protected]",
balance: "300.00",
status: "inactive",
createdAt: "2024-01-16T15:45:00Z",
tags: ["Trial"],
},
];
const formatValue = (value: string, prop: string, item: User) => {
switch (prop) {
case "balance":
return `$${Number(value).toFixed(2)}`;
case "status":
return (
<span
className={`px-2 py-1 rounded-full text-xs ${
value === "active"
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{value}
</span>
);
default:
return value;
}
};
const handleRowClick = (user: User) => {
console.log("Clicked user:", user);
};
return (
<TableComponent<User>
columns={[
"ID",
"Name",
"Email",
"Balance",
"Status",
"Created At",
"Tags",
]}
data={data}
props={["id", "name", "email", "balance", "status", "createdAt", "tags"]}
formatValue={formatValue}
sortableProps={["name", "balance", "status", "createdAt"]}
rowOnClick={handleRowClick}
enableDarkMode={true}
/>
);
};
export default MyTable;
The Next.js Reusable Table component is a highly customizable, TypeScript-ready, and production-grade solution for displaying tabular data within Next.js applications. It is designed to handle diverse data structures, integrate smoothly with your styling preferences, and provide a feature set that streamlines data visualization, user interaction, and responsive design.
By adhering to industry standards and best practices, this component ensures maintainability, performance, and ease of integration into both small and large-scale Next.js projects. You can leverage its built-in search, pagination, sorting, formatting, and action dropdown features while maintaining full control over styling and rendering.
Use this documentation as a comprehensive guide to seamlessly integrate the Next.js Reusable Table into your workflow, enhance your frontend data management capabilities, and offer end-users a polished, intuitive interface for exploring tabular information.
Each column header includes a dropdown menu (⋮) with the following options:
- Stick/unstick horizontally (right-click header)
- Stick/unstick vertically (shift + right-click header)
- Hide/show columns
- Sort columns (when enabled)
<TableComponent<User>
columns={columns}
data={data}
props={props}
sortableProps={["name", "email", "createdAt"]} // Enable sorting for these columns
/>
The table provides intelligent click handling:
- Click anywhere on a row to trigger row action
- Click on cell content to expand/interact without triggering row action
- Expandable content with "show more" functionality
<TableComponent<User>
// ... other props
rowOnClick={(user) => console.log("Row clicked:", user)}
formatValue={(value, prop, item) => {
if (prop === "description") {
return <div className="hover:bg-gray-50 cursor-pointer">{value}</div>;
}
return value;
}}
/>
The table automatically handles different data types:
Arrays are displayed as chips with expand/collapse functionality:
interface Item {
tags: string[];
}
const data = [
{
tags: ["one", "two", "three", "four", "five", "six"],
},
];
// Tags will show first 5 items with "+1 more" button
Automatic date formatting:
interface Item {
createdAt: string;
}
const data = [
{
createdAt: "2024-01-15T10:30:00Z", // Will be formatted as "Jan 15, 2024 10:30 AM"
},
];
Automatic link detection and formatting:
interface Item {
website: string;
}
const data = [
{
website: "https://example.com", // Will be rendered as clickable link
},
];
Add row actions with dropdown menu:
<TableComponent<User>
// ... other props
actions={true}
actionTexts={["Edit", "Delete", "View Details"]}
actionFunctions={[
(user) => handleEdit(user),
(user) => handleDelete(user),
(user) => handleView(user),
]}
/>
Built-in search and pagination support:
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
<TableComponent<User>
// ... other props
searchValue={searchTerm}
enablePagination={true}
page={page}
setPage={setPage}
itemsPerPage={10}
/>;
Customize appearance with Tailwind classes:
const customClassNames = {
table: "shadow-lg border-2 border-gray-200",
thead: "bg-gray-50",
tbody: "divide-y divide-gray-200",
th: "px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",
tr: "hover:bg-gray-50",
td: "px-4 py-2",
actionButton: "text-gray-600 hover:text-gray-900",
pagination: {
container: "mt-4",
button: "px-3 py-1 bg-gray-200 rounded",
buttonDisabled: "opacity-50",
pageInfo: "mx-2",
},
};
<TableComponent<User>
// ... other props
customClassNames={customClassNames}
disableDefaultStyles={false} // Set to true to use only custom classes
/>;
Built-in dark mode support that respects system preferences:
<TableComponent<User>
// ... other props
enableDarkMode={true} // Will automatically switch based on system preference
/>
Show loading skeleton:
<TableComponent<User>
// ... other props
loading={true}
/>
Custom empty state message:
<TableComponent<User>
// ... other props
noContentProps={{
text: "No users found",
name: "users",
icon: <CustomIcon />, // Optional
}}
/>
Prop | Type | Required | Default | Description |
---|---|---|---|---|
columns | string[] | Yes | - | Column headers |
data | T[] | Yes | - | Array of data objects |
props | ReadonlyArray | Yes | - | Object keys to display |
loading | boolean | No | false | Show loading state |
searchValue | string | No | - | Filter value for rows |
Prop | Type | Default | Description |
---|---|---|---|
sortableProps | Array | [] | Columns that can be sorted |
formatValue | Function | - | Custom value formatter |
Prop | Type | Default | Description |
---|---|---|---|
rowOnClick | (item: T) => void | - | Row click handler |
actions | boolean | false | Enable action dropdown |
actionTexts | string[] | - | Action dropdown labels |
actionFunctions | Array | - | Action dropdown handlers |
Prop | Type | Default | Description |
---|---|---|---|
disableDefaultStyles | boolean | false | Disable built-in styles |
customClassNames | Object | {} | Custom class names |
enableDarkMode | boolean | true | Enable dark mode |
Prop | Type | Default | Description |
---|---|---|---|
enablePagination | boolean | false | Enable pagination |
page | number | 1 | Current page |
itemsPerPage | number | 10 | Items per page |
totalPages | number | - | Total pages override |
interface TableProps<T> {
columns: string[];
data: T[];
props: ReadonlyArray<keyof T>;
actions?: boolean;
actionTexts?: string[];
loading?: boolean;
actionFunctions?: Array<(item: T) => void>;
searchValue?: string;
disableDefaultStyles?: boolean;
customClassNames?: {
container?: string;
table?: string;
thead?: string;
tbody?: string;
th?: string;
tr?: string;
td?: string;
actionTd?: string;
actionButton?: string;
actionSvg?: string;
dropdownMenu?: string;
dropdownItem?: string;
pagination?: {
container?: string;
button?: string;
buttonDisabled?: string;
pageInfo?: string;
};
};
renderRow?: (item: T, index: number) => React.ReactNode;
rowOnClick?: (item: T) => void;
enableDarkMode?: boolean;
enablePagination?: boolean;
page?: number;
setPage?: (page: number) => void;
itemsPerPage?: number;
totalPages?: number;
sortableProps?: Array<keyof T>;
formatValue?: (value: string, prop: string, item: T) => React.ReactNode;
noContentProps?: {
text?: string;
icon?: React.ReactNode;
name?: string;
};
}
"use client";
import React, { useState } from "react";
import "nextjs-reusable-table/dist/index.css";
import { TableComponent } from "nextjs-reusable-table";
interface User {
id: string;
name: string;
email: string;
joinDate: string;
status: string;
roles: string[];
lastLogin: string;
profileUrl: string;
tags: string[];
department: string;
}
const Test = () => {
const [page, setPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const sampleData: User[] = [
{
id: "1",
name: "John Doe",
email: "[email protected]",
joinDate: "2023-01-15T10:30:00Z",
status: "Active",
roles: ["Admin", "Editor", "User"],
lastLogin: "2024-01-20T15:45:00Z",
profileUrl: "https://example.com/john",
tags: ["VIP", "Early Adopter"],
department: "Engineering",
},
{
id: "2",
name: "Jane Smith",
email: "[email protected]",
joinDate: "2023-02-20T09:15:00Z",
status: "Active",
roles: ["User", "Support"],
lastLogin: "2024-01-19T12:30:00Z",
profileUrl: "https://example.com/jane",
tags: ["Support Team"],
department: "Customer Support",
},
{
id: "3",
name: "Bob Johnson",
email: "[email protected]",
joinDate: "2023-03-10T14:20:00Z",
status: "Inactive",
roles: ["User"],
lastLogin: "2023-12-15T10:00:00Z",
profileUrl: "https://example.com/bob",
tags: ["New User"],
department: "Marketing",
},
{
id: "4",
name: "Sarah Wilson",
email: "[email protected]",
joinDate: "2023-04-05T11:45:00Z",
status: "Active",
roles: ["Editor", "User", "Content Manager"],
lastLogin: "2024-01-21T09:15:00Z",
profileUrl: "https://example.com/sarah",
tags: ["Content Team", "VIP"],
department: "Content",
},
{
id: "5",
name: "Mike Brown",
email: "[email protected]",
joinDate: "2023-05-12T13:10:00Z",
status: "Active",
roles: ["User", "Analytics"],
lastLogin: "2024-01-18T16:20:00Z",
profileUrl: "https://example.com/mike",
tags: ["Analytics Team"],
department: "Data Science",
},
];
const columns = [
"ID",
"Name",
"Email",
"Join Date",
"Status",
"Roles",
"Last Login",
"Profile",
"Tags",
"Department",
];
const props: Array<keyof User> = [
"id",
"name",
"email",
"joinDate",
"status",
"roles",
"lastLogin",
"profileUrl",
"tags",
"department",
];
const actionTexts = ["Edit", "Delete", "View Details"];
const actionFunctions = [
(user: User) => console.log(`Edit ${user.name}`),
(user: User) => console.log(`Delete ${user.name}`),
(user: User) => console.log(`View ${user.name}'s details`),
];
const customFormatValue = (value: string, prop: string, item: User) => {
switch (prop) {
case "status":
return (
<span
className={`px-2 py-1 rounded-full text-xs ${
value === "Active"
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{value}
</span>
);
case "department":
return (
<span className="font-medium text-gray-900 dark:text-gray-100">
{value}
</span>
);
case "profileUrl":
return (
<a
href={value}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
onClick={(e) => e.stopPropagation()}
>
View Profile
</a>
);
default:
return value;
}
};
const handleRowClick = (user: User) => {
console.log(`Clicked row for ${user.name}`);
};
const customClassNames = {
table: "min-w-full divide-y divide-gray-200 dark:divide-gray-700",
thead: "bg-gray-50 dark:bg-gray-800",
tbody:
"bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700",
th: "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider",
tr: "hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-200",
td: "px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300",
actionButton:
"text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white",
actionSvg: "w-5 h-5",
dropdownMenu:
"bg-white dark:bg-gray-800 shadow-lg rounded-md border dark:border-gray-700",
dropdownItem:
"px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700",
pagination: {
container:
"bg-white dark:bg-gray-800 px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-gray-700 sm:px-6",
button:
"relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700",
buttonDisabled: "opacity-50 cursor-not-allowed",
pageInfo: "text-sm text-gray-700 dark:text-gray-300",
},
};
return (
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="px-4 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300"
/>
<div className="text-sm text-gray-600 dark:text-gray-400">
Tip: Right-click column headers for sticky options
</div>
</div>
<div className="border rounded-lg dark:border-gray-700 overflow-hidden">
<TableComponent<User>
columns={columns}
data={sampleData}
props={props}
actions={true}
actionTexts={actionTexts}
actionFunctions={actionFunctions}
searchValue={searchTerm}
enablePagination={true}
page={page}
setPage={setPage}
itemsPerPage={5}
sortableProps={[
"name",
"email",
"joinDate",
"status",
"department",
"lastLogin",
]}
formatValue={customFormatValue}
rowOnClick={handleRowClick}
enableDarkMode={true}
customClassNames={customClassNames}
noContentProps={{
text: "No users found",
name: "users",
}}
/>
</div>
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400">
<p>
<strong>Features demonstrated:</strong>
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
Column Header Management (click ⋮ icon):
<ul className="list-disc pl-5 mt-1">
<li>Hide/Show columns</li>
<li>Toggle sticky horizontal/vertical</li>
<li>Sort columns</li>
</ul>
</li>
<li>
Smart Row Interaction:
<ul className="list-disc pl-5 mt-1">
<li>Click row area for row action</li>
<li>Click cell content to expand/interact</li>
<li>Protected link and action clicks</li>
</ul>
</li>
<li>
Enhanced Data Display:
<ul className="list-disc pl-5 mt-1">
<li>Array chips with expand/collapse</li>
<li>Formatted dates</li>
<li>Status badges</li>
<li>Clickable URLs</li>
</ul>
</li>
<li>
Visual Features:
<ul className="list-disc pl-5 mt-1">
<li>Dark mode support</li>
<li>Loading skeleton</li>
<li>Custom cell styling</li>
<li>Responsive layout</li>
</ul>
</li>
<li>
Functionality:
<ul className="list-disc pl-5 mt-1">
<li>Search filtering</li>
<li>Pagination</li>
<li>Action dropdown</li>
<li>Column sorting</li>
</ul>
</li>
</ul>
</div>
</div>
);
};
export default Test;
Contributions are welcome! Please see CONTRIBUTING.md for details on how to get started.
We use Semantic Versioning for versioning. For the versions available, see the tags on this repository.
To bump the version, update the version
field in package.json
and follow the guidelines in the CONTRIBUTING.md file.
This project is licensed under the ISC License - see the LICENSE file for details.
This project adheres to the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.
- Inspired by common data table patterns in React and Next.js applications.
- Thanks to all contributors and users for their support.