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
ReactTailwind
TSX
npx @uiblox/cli add data-table

Preview

Layout:

Team Members

4 users

UserRoleStatus
SA

Sarah Anderson

sarah@example.com

Adminactive
MC

Michael Chen

michael@example.com

Developeractive
EJ

Emily Johnson

emily@example.com

Designerpending
DK

David Kim

david@example.com

Developerinactive

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"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>; }