Data Tables
Build powerful, feature-rich data tables with sorting, filtering, pagination, and more.
Interactive Demo
Try out the data table below. Use the search, sort columns, and try the row actions:
| Actions | ||||
|---|---|---|---|---|
| John Doe | john@example.com | Admin | Active | |
| Jane Smith | jane@example.com | User | Active | |
| Bob Johnson | bob@example.com | Moderator | Inactive | |
| Alice Williams | alice@example.com | User | Active | |
| Charlie Brown | charlie@example.com | User | Active | |
| Diana Prince | diana@example.com | Admin | Active | |
| Ethan Hunt | ethan@example.com | User | Inactive | |
| Fiona Carter | fiona@example.com | Moderator | Active |
Introduction
ShadPanel’s Data Table component provides a declarative API for building tables with:
- ✅ Declarative Columns - Define columns with JSX components
- ✅ Built-in Sorting - Click headers to sort ascending/descending
- ✅ Global Search - Search across all searchable columns
- ✅ Column Filtering - Filter individual columns
- ✅ Pagination - Built-in pagination controls
- ✅ Row Selection - Select single or multiple rows
- ✅ Row Actions - Actions menu for each row
- ✅ Column Visibility - Show/hide columns dynamically
- ✅ TypeScript Support - Full type safety with generics
- ✅ Responsive - Mobile-friendly design
Quick Start
Basic Table
'use client'
import { Table, TableTextColumn } from '@/components/ui'
const users = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User' },
]
export default function UsersTable() {
return (
<Table data={users}>
<TableTextColumn accessor="name" header="Name" />
<TableTextColumn accessor="email" header="Email" />
<TableTextColumn accessor="role" header="Role" />
</Table>
)
}With Sorting and Search
<Table data={users}>
<TableTextColumn accessor="name" header="Name" sortable searchable />
<TableTextColumn accessor="email" header="Email" searchable />
<TableTextColumn accessor="role" header="Role" sortable />
</Table>With Actions
import { Table, TableTextColumn, TableActionsColumn, TableAction } from '@/components/ui'
import { Edit, Trash, Eye } from 'lucide-react'
export default function UsersTable({ users }) {
return (
<Table data={users}>
<TableTextColumn accessor="name" header="Name" sortable searchable />
<TableTextColumn accessor="email" header="Email" searchable />
<TableTextColumn accessor="role" header="Role" sortable />
<TableActionsColumn>
<TableAction
icon={Eye}
label="View"
onClick={(row) => console.log('View', row)}
/>
<TableAction
icon={Edit}
label="Edit"
onClick={(row) => console.log('Edit', row)}
/>
<TableAction
icon={Trash}
label="Delete"
variant="destructive"
onClick={(row) => console.log('Delete', row)}
/>
</TableActionsColumn>
</Table>
)
}Column Types
TableTextColumn
Display text with optional sorting and filtering:
<TableTextColumn
accessor="name"
header="Full Name"
sortable
searchable
/>Props:
accessor- Data key (required)header- Column header text (required)sortable- Enable sorting (optional)searchable- Enable column filtering (optional)
TableImageColumn
Display images:
<TableImageColumn
accessor="avatar"
header="Avatar"
width={40}
height={40}
alt="User avatar"
/>Props:
accessor- Image URL key (required)header- Column header (required)width- Image width (optional, default: 32)height- Image height (optional, default: 32)alt- Alt text (optional)
TableSelectColumn
Add row selection checkboxes:
<Table data={users}>
<TableSelectColumn />
<TableTextColumn accessor="name" header="Name" />
<TableTextColumn accessor="email" header="Email" />
</Table>Access selected rows:
import { useState } from 'react'
export default function UsersTable({ users }) {
const [selectedRows, setSelectedRows] = useState([])
return (
<Table
data={users}
onSelectionChange={(rows) => setSelectedRows(rows)}
>
<TableSelectColumn />
<TableTextColumn accessor="name" header="Name" />
</Table>
)
}TableActionsColumn
Add action buttons for each row:
<TableActionsColumn>
<TableAction icon={Edit} label="Edit" onClick={(row) => {}} />
<TableAction icon={Trash} label="Delete" variant="destructive" onClick={(row) => {}} />
</TableActionsColumn>TableAction Props:
icon- Lucide icon component (required)label- Action label (required)onClick- Click handler with row data (required)variant- Button variant:default,destructive(optional)
Features
Sorting
Enable sorting by adding sortable prop:
<TableTextColumn accessor="name" header="Name" sortable />Behavior:
- Click header to sort ascending
- Click again to sort descending
- Click again to remove sorting
Global Search
Built-in search across all searchable columns:
<Table data={users}>
<TableTextColumn accessor="name" header="Name" searchable />
<TableTextColumn accessor="email" header="Email" searchable />
<TableTextColumn accessor="role" header="Role" />
</Table>The search box appears automatically when at least one column is searchable.
Column Filtering
Each searchable column gets its own filter:
<TableTextColumn accessor="role" header="Role" searchable />Filter inputs appear in column headers.
Pagination
Automatic pagination with page size controls:
<Table data={users} />Default: 10 rows per page
Page sizes: 10, 20, 30, 40, 50
Row Selection
Select single or multiple rows:
<Table data={users} onSelectionChange={(rows) => console.log(rows)}>
<TableSelectColumn />
{/* Columns */}
</Table>Features:
- Individual row selection
- Select all on current page
- Deselect all
Column Visibility
Toggle column visibility:
<Table data={users}>
<TableTextColumn accessor="name" header="Name" />
<TableTextColumn accessor="email" header="Email" />
<TableTextColumn accessor="phone" header="Phone" /> {/* Can be hidden */}
</Table>A “Columns” dropdown appears automatically for toggling visibility.
TypeScript Support
Use generics for type safety:
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
const users: User[] = [
// ...
]
<Table<User> data={users}>
<TableTextColumn accessor="name" header="Name" />
<TableTextColumn accessor="email" header="Email" />
<TableTextColumn accessor="role" header="Role" />
</Table>TypeScript will enforce that accessor matches keys of User.
Custom Cell Rendering
Using Cell Prop
<TableTextColumn
accessor="status"
header="Status"
cell={(value) => (
<span className={value === 'active' ? 'text-green-600' : 'text-red-600'}>
{value}
</span>
)}
/>Using Row Data
<TableTextColumn
accessor="name"
header="Name"
cell={(value, row) => (
<div className="flex items-center gap-2">
<img src={row.avatar} alt={value} className="w-8 h-8 rounded-full" />
<span>{value}</span>
</div>
)}
/>Complete Example
'use client'
import { useState } from 'react'
import {
Table,
TableTextColumn,
TableImageColumn,
TableSelectColumn,
TableActionsColumn,
TableAction,
} from '@/components/ui'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Edit, Trash, Eye, Plus } from 'lucide-react'
interface User {
id: number
name: string
email: string
avatar: string
role: 'admin' | 'user' | 'moderator'
status: 'active' | 'inactive'
createdAt: string
}
const users: User[] = [
{
id: 1,
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatars/john.jpg',
role: 'admin',
status: 'active',
createdAt: '2024-01-15',
},
// More users...
]
export default function UsersTable() {
const [selectedRows, setSelectedRows] = useState<User[]>([])
const handleDelete = async (user: User) => {
if (confirm(`Delete ${user.name}?`)) {
// API call to delete
console.log('Deleting:', user)
}
}
const handleBulkDelete = async () => {
if (confirm(`Delete ${selectedRows.length} users?`)) {
// API call to bulk delete
console.log('Bulk deleting:', selectedRows)
}
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Users</h2>
<div className="flex gap-2">
{selectedRows.length > 0 && (
<Button variant="destructive" onClick={handleBulkDelete}>
Delete {selectedRows.length} users
</Button>
)}
<Button>
<Plus className="w-4 h-4 mr-2" />
Add User
</Button>
</div>
</div>
<Table<User>
data={users}
onSelectionChange={setSelectedRows}
>
<TableSelectColumn />
<TableImageColumn
accessor="avatar"
header="Avatar"
width={40}
height={40}
/>
<TableTextColumn
accessor="name"
header="Name"
sortable
searchable
/>
<TableTextColumn
accessor="email"
header="Email"
searchable
/>
<TableTextColumn
accessor="role"
header="Role"
sortable
cell={(value) => (
<Badge variant={value === 'admin' ? 'default' : 'secondary'}>
{value}
</Badge>
)}
/>
<TableTextColumn
accessor="status"
header="Status"
sortable
cell={(value) => (
<Badge variant={value === 'active' ? 'success' : 'destructive'}>
{value}
</Badge>
)}
/>
<TableTextColumn
accessor="createdAt"
header="Created"
sortable
cell={(value) => new Date(value).toLocaleDateString()}
/>
<TableActionsColumn>
<TableAction
icon={Eye}
label="View"
onClick={(row) => console.log('View:', row)}
/>
<TableAction
icon={Edit}
label="Edit"
onClick={(row) => console.log('Edit:', row)}
/>
<TableAction
icon={Trash}
label="Delete"
variant="destructive"
onClick={handleDelete}
/>
</TableActionsColumn>
</Table>
</div>
)
}Styling
Custom Class Names
<Table data={users} className="my-custom-table">
<TableTextColumn accessor="name" header="Name" />
</Table>Dark Mode
Table automatically supports dark mode:
// Automatically adapts to theme
<Table data={users}>
{/* Columns */}
</Table>Best Practices
Performance
For large datasets, consider:
-
Server-side pagination:
// Fetch paginated data from API const { data, total } = await fetchUsers({ page, pageSize }) -
Memoization:
const users = useMemo(() => fetchedUsers, [fetchedUsers]) -
Virtual scrolling for very large lists
Accessibility
The table includes:
- ✅ Proper ARIA labels
- ✅ Keyboard navigation
- ✅ Screen reader support
- ✅ Focus management
Mobile Responsiveness
Table automatically adapts to mobile:
- Horizontal scrolling on small screens
- Responsive controls
- Touch-friendly actions
Next Steps
- Column Types - Detailed column reference
- Table Features - Advanced features
- Table API - Complete API reference
- Table Examples - Real-world examples