Blox/
Data Table
A feature-rich data table with sorting, filtering, pagination, and row selection. Essential for admin dashboards and data management interfaces.
SortingFilteringPaginationSelection
Installation
Terminal
TSXReactTailwind
npx @uiblox/cli add data-tablePreview
Layout:
Team Members
4 users
| User | Role | Status | ||
|---|---|---|---|---|
SA Sarah Anderson sarah@example.com | Admin | active | ||
MC Michael Chen michael@example.com | Developer | active | ||
EJ Emily Johnson emily@example.com | Designer | pending | ||
DK David Kim david@example.com | Developer | inactive |
1-4 of 4
Use Cases
User Management
Manage users, roles, and permissions with bulk actions and filtering.
Order History
Display and filter orders by status, date, and customer information.
Content Management
Organize blog posts, products, or media with sortable columns.
Analytics Reports
Present metrics and statistics in a structured, exportable format.
Source Code
data-table.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220"use client"; import { useState, useMemo } from "react"; import { cn } from "@/lib/utils"; interface User { id: string; name: string; email: string; role: string; status: "active" | "inactive" | "pending"; joinDate: string; avatar: string; } const users: User[] = [ { id: "1", name: "Sarah Anderson", email: "sarah@example.com", role: "Admin", status: "active", joinDate: "Jan 15, 2024", avatar: "SA" }, { id: "2", name: "Michael Chen", email: "michael@example.com", role: "Developer", status: "active", joinDate: "Feb 3, 2024", avatar: "MC" }, { id: "3", name: "Emily Johnson", email: "emily@example.com", role: "Designer", status: "pending", joinDate: "Mar 22, 2024", avatar: "EJ" }, { id: "4", name: "David Kim", email: "david@example.com", role: "Developer", status: "active", joinDate: "Apr 10, 2024", avatar: "DK" }, { id: "5", name: "Lisa Wang", email: "lisa@example.com", role: "Marketing", status: "inactive", joinDate: "May 5, 2024", avatar: "LW" }, { id: "6", name: "James Brown", email: "james@example.com", role: "Support", status: "active", joinDate: "Jun 18, 2024", avatar: "JB" }, { id: "7", name: "Anna Martinez", email: "anna@example.com", role: "Designer", status: "active", joinDate: "Jul 2, 2024", avatar: "AM" }, { id: "8", name: "Robert Taylor", email: "robert@example.com", role: "Developer", status: "pending", joinDate: "Aug 14, 2024", avatar: "RT" }, ]; type SortKey = keyof User; type SortDirection = "asc" | "desc"; export function DataTable() { const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState<string>("all"); const [sortKey, setSortKey] = useState<SortKey>("name"); const [sortDirection, setSortDirection] = useState<SortDirection>("asc"); const [selectedRows, setSelectedRows] = useState<string[]>([]); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 5; const filteredData = useMemo(() => { return users .filter((user) => { const matchesSearch = user.name.toLowerCase().includes(searchQuery.toLowerCase()) || user.email.toLowerCase().includes(searchQuery.toLowerCase()); const matchesStatus = statusFilter === "all" || user.status === statusFilter; return matchesSearch && matchesStatus; }) .sort((a, b) => { const aValue = a[sortKey]; const bValue = b[sortKey]; if (aValue < bValue) return sortDirection === "asc" ? -1 : 1; if (aValue > bValue) return sortDirection === "asc" ? 1 : -1; return 0; }); }, [searchQuery, statusFilter, sortKey, sortDirection]); const paginatedData = filteredData.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage ); const totalPages = Math.ceil(filteredData.length / itemsPerPage); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { setSortKey(key); setSortDirection("asc"); } }; const toggleSelectAll = () => { if (selectedRows.length === paginatedData.length) { setSelectedRows([]); } else { setSelectedRows(paginatedData.map((user) => user.id)); } }; const toggleSelectRow = (id: string) => { setSelectedRows((prev) => prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id] ); }; const getStatusBadge = (status: User["status"]) => { const styles = { active: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", inactive: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-400", pending: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400", }; return ( <span className={`px-2.5 py-1 text-xs font-medium rounded-full ${styles[status]}`}> {status.charAt(0).toUpperCase() + status.slice(1)} </span> ); }; return ( <div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800 overflow-hidden"> {/* Header */} <div className="p-6 border-b border-slate-200 dark:border-slate-800"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div> <h2 className="text-xl font-semibold text-slate-900 dark:text-white">Team Members</h2> <p className="text-sm text-slate-500 mt-1">{filteredData.length} users found</p> </div> <button className="px-4 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-xl flex items-center gap-2"> <PlusIcon /> Add Member </button> </div> {/* Filters */} <div className="flex flex-col sm:flex-row gap-3 mt-4"> <div className="relative flex-1"> <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" /> <input type="text" placeholder="Search by name or email..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 border border-slate-200 dark:border-slate-700 rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-purple-500" /> </div> <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="px-4 py-2.5 border border-slate-200 dark:border-slate-700 rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-purple-500" > <option value="all">All Status</option> <option value="active">Active</option> <option value="inactive">Inactive</option> <option value="pending">Pending</option> </select> </div> </div> {/* Table */} <div className="overflow-x-auto"> <table className="w-full"> <thead> <tr className="border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50"> <th className="px-6 py-4 text-left"> <input type="checkbox" checked={selectedRows.length === paginatedData.length && paginatedData.length > 0} onChange={toggleSelectAll} className="w-4 h-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500" /> </th> <th className="px-6 py-4 text-left"> <button onClick={() => handleSort("name")} className="flex items-center gap-2 text-xs font-semibold text-slate-500 uppercase tracking-wide hover:text-slate-700"> User <SortIcon active={sortKey === "name"} direction={sortDirection} /> </button> </th> <th className="px-6 py-4 text-left"> <button onClick={() => handleSort("role")} className="flex items-center gap-2 text-xs font-semibold text-slate-500 uppercase tracking-wide hover:text-slate-700"> Role <SortIcon active={sortKey === "role"} direction={sortDirection} /> </button> </th> <th className="px-6 py-4 text-left"> <button onClick={() => handleSort("status")} className="flex items-center gap-2 text-xs font-semibold text-slate-500 uppercase tracking-wide hover:text-slate-700"> Status <SortIcon active={sortKey === "status"} direction={sortDirection} /> </button> </th> <th className="px-6 py-4 text-left"> <span className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Joined</span> </th> <th className="px-6 py-4 text-right"> <span className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Actions</span> </th> </tr> </thead> <tbody> {paginatedData.map((user) => ( <tr key={user.id} className={cn("border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors", selectedRows.includes(user.id) && "bg-purple-50 dark:bg-purple-900/10")}> <td className="px-6 py-4"> <input type="checkbox" checked={selectedRows.includes(user.id)} onChange={() => toggleSelectRow(user.id)} className="w-4 h-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500" /> </td> <td className="px-6 py-4"> <div className="flex items-center gap-3"> <div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-white font-semibold text-sm">{user.avatar}</div> <div> <p className="font-medium text-slate-900 dark:text-white">{user.name}</p> <p className="text-sm text-slate-500">{user.email}</p> </div> </div> </td> <td className="px-6 py-4 text-sm text-slate-600 dark:text-slate-400">{user.role}</td> <td className="px-6 py-4">{getStatusBadge(user.status)}</td> <td className="px-6 py-4 text-sm text-slate-500">{user.joinDate}</td> <td className="px-6 py-4 text-right"> <button className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg"><MoreIcon /></button> </td> </tr> ))} </tbody> </table> </div> {/* Pagination */} <div className="flex items-center justify-between px-6 py-4 border-t border-slate-200 dark:border-slate-800"> <p className="text-sm text-slate-500">Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, filteredData.length)} of {filteredData.length}</p> <div className="flex items-center gap-2"> <button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))} disabled={currentPage === 1} className="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-lg text-sm disabled:opacity-50">Previous</button> {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( <button key={page} onClick={() => setCurrentPage(page)} className={cn("w-10 h-10 rounded-lg text-sm font-medium", currentPage === page ? "bg-purple-600 text-white" : "hover:bg-slate-100 dark:hover:bg-slate-800")}>{page}</button> ))} <button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))} disabled={currentPage === totalPages} className="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-lg text-sm disabled:opacity-50">Next</button> </div> </div> </div> ); } function PlusIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>; } function SearchIcon({ className }: { className?: string }) { return <svg className={cn("w-5 h-5", className)} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>; } function MoreIcon() { return <svg className="w-5 h-5 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" /></svg>; } function SortIcon({ active, direction }: { active: boolean; direction: SortDirection }) { return <svg className={cn("w-4 h-4", active ? "text-purple-600" : "text-slate-400")} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={direction === "asc" ? "M5 15l7-7 7 7" : "M19 9l-7 7-7-7"} /></svg>; }