Components
Data Table

Data Table

Interactive data table with sorting, filtering, pagination, and search functionality. Perfect for dashboards and admin panels.

UserRoleStatusActions
JD
John Doe
john.doe@example.com
AdminActive
SW
Sarah Wilson
sarah.wilson@example.com
EditorActive
MJ
Mike Johnson
mike.johnson@example.com
UserInactive
ED
Emily Davis
emily.davis@example.com
EditorPending
AC
Alex Chen
alex.chen@example.com
UserActive
Showing 1 to 5 of 5 results

Installation

1

Install the packages

npm i motion lucide-react clsx tailwind-merge next-themes
2

Add util file

lib/util.ts
import { ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
3

Copy and paste the following code into your project

data-table.tsx
"use client"; import React, { useState, useMemo } from "react"; import { motion } from "motion/react"; import { useTheme } from "next-themes"; import Image from "next/image"; import { Search, ChevronLeft, ChevronRight, Eye, Edit, Trash2 } from "lucide-react"; interface User { id: number; name: string; email: string; role: string; status: "Active" | "Inactive" | "Pending"; joinDate: string; avatar: string; } const sampleData: User[] = [ { id: 1, name: "John Doe", email: "john.doe@example.com", role: "Admin", status: "Active", joinDate: "2024-01-15", avatar: "https://avatar.iran.liara.run/public/10" }, { id: 2, name: "Sarah Wilson", email: "sarah.wilson@example.com", role: "Editor", status: "Active", joinDate: "2024-02-20", avatar: "https://avatar.iran.liara.run/public/47" }, { id: 3, name: "Mike Johnson", email: "mike.johnson@example.com", role: "User", status: "Inactive", joinDate: "2024-01-10", avatar: "https://avatar.iran.liara.run/public/42" }, { id: 4, name: "Emily Davis", email: "emily.davis@example.com", role: "Editor", status: "Pending", joinDate: "2024-03-05", avatar: "https://avatar.iran.liara.run/public/41" }, { id: 5, name: "Alex Chen", email: "alex.chen@example.com", role: "User", status: "Active", joinDate: "2024-02-28", avatar: "https://avatar.iran.liara.run/public/27" } ]; export default function DataTable({ data = sampleData, itemsPerPage = 5 }: { data?: User[]; itemsPerPage?: number }) { const { theme } = useTheme(); const [searchTerm, setSearchTerm] = useState(""); const [currentPage, setCurrentPage] = useState(1); const filteredData = useMemo(() => { return data.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()) || user.email.toLowerCase().includes(searchTerm.toLowerCase()) ); }, [data, searchTerm]); const totalPages = Math.ceil(filteredData.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const paginatedData = filteredData.slice(startIndex, startIndex + itemsPerPage); const isDark = theme === 'dark'; return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className={`rounded-lg border ${isDark ? 'bg-neutral-950 border-neutral-800' : 'bg-white border-gray-200'} overflow-hidden`} > {/* Search */} <div className={`p-4 border-b ${isDark ? 'border-neutral-800' : 'border-gray-200'}`}> <div className="relative"> <Search className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? 'text-neutral-400' : 'text-gray-400'}`} /> <input type="text" placeholder="Search users..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className={`w-full pl-10 pr-4 py-2 rounded-lg border ${ isDark ? 'bg-neutral-900 border-neutral-700 text-neutral-100 placeholder-neutral-500' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' } focus:outline-none focus:ring-2 focus:ring-cyan-500`} /> </div> </div> {/* Table */} <div className="overflow-x-auto"> <table className="w-full"> <thead className={isDark ? 'bg-neutral-900' : 'bg-gray-50'}> <tr> <th className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-neutral-300' : 'text-gray-500'}`}>User</th> <th className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-neutral-300' : 'text-gray-500'}`}>Role</th> <th className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-neutral-300' : 'text-gray-500'}`}>Status</th> <th className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-neutral-300' : 'text-gray-500'}`}>Actions</th> </tr> </thead> <tbody className={`divide-y ${isDark ? 'divide-neutral-800' : 'divide-gray-200'}`}> {paginatedData.map((user) => ( <motion.tr key={user.id} whileHover={{ backgroundColor: isDark ? 'rgb(23 23 23)' : '#f9fafb' }}> <td className="px-6 py-4 whitespace-nowrap"> <div className="flex items-center"> <Image className="h-10 w-10 rounded-full" src={user.avatar} alt={user.name} width={40} height={40} /> <div className="ml-4"> <div className={`text-sm font-medium ${isDark ? 'text-neutral-100' : 'text-gray-900'}`}>{user.name}</div> <div className={`text-sm ${isDark ? 'text-neutral-400' : 'text-gray-500'}`}>{user.email}</div> </div> </div> </td> <td className={`px-6 py-4 whitespace-nowrap text-sm ${isDark ? 'text-neutral-100' : 'text-gray-900'}`}>{user.role}</td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${ user.status === 'Active' ? 'bg-green-100 text-green-800' : user.status === 'Inactive' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800' }`}> {user.status} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm"> <div className="flex space-x-2"> <button className={`p-1 rounded hover:bg-gray-100 ${isDark ? 'text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800' : 'text-gray-400'}`}> <Eye className="w-4 h-4" /> </button> <button className={`p-1 rounded hover:bg-gray-100 ${isDark ? 'text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800' : 'text-gray-400'}`}> <Edit className="w-4 h-4" /> </button> <button className={`p-1 rounded hover:bg-gray-100 ${isDark ? 'text-neutral-400 hover:text-red-400 hover:bg-neutral-800' : 'text-gray-400'}`}> <Trash2 className="w-4 h-4" /> </button> </div> </td> </motion.tr> ))} </tbody> </table> </div> {/* Pagination */} <div className={`px-6 py-4 border-t ${isDark ? 'bg-neutral-900 border-neutral-800' : 'bg-gray-50 border-gray-200'}`}> <div className="flex items-center justify-between"> <div className={`text-sm ${isDark ? 'text-neutral-400' : 'text-gray-700'}`}> Showing {startIndex + 1} to {Math.min(startIndex + itemsPerPage, filteredData.length)} of {filteredData.length} results </div> <div className="flex items-center gap-2"> <button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className={`flex items-center gap-1 px-3 py-2 text-sm rounded-lg disabled:opacity-50 ${ isDark ? 'text-neutral-300 border-neutral-700 hover:bg-neutral-800' : 'text-gray-500 border-gray-300 hover:bg-gray-50' } border`} > <ChevronLeft className="w-4 h-4" /> Previous </button> <button onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages} className={`flex items-center gap-1 px-3 py-2 text-sm rounded-lg disabled:opacity-50 ${ isDark ? 'text-neutral-300 border-neutral-700 hover:bg-neutral-800' : 'text-gray-500 border-gray-300 hover:bg-gray-50' } border`} > Next <ChevronRight className="w-4 h-4" /> </button> </div> </div> </div> </motion.div> ); }
4

Update the import paths to match your project setup

Props

PropTypeDefaultDescription
dataUser[]sampleDataArray of user objects to display in the table.
itemsPerPagenumber5Number of items to display per page.