Skip to Content
ComponentsData Tables

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:

EmailActions
John Doejohn@example.comAdminActive
Jane Smithjane@example.comUserActive
Bob Johnsonbob@example.comModeratorInactive
Alice Williamsalice@example.comUserActive
Charlie Browncharlie@example.comUserActive
Diana Princediana@example.comAdminActive
Ethan Huntethan@example.comUserInactive
Fiona Carterfiona@example.comModeratorActive
Rows per page
Showing 8 of 8 row(s)
Page 1 of 1

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> ) }
<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)

View Column Reference →

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

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.

View Features Guide →

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:

  1. Server-side pagination:

    // Fetch paginated data from API const { data, total } = await fetchUsers({ page, pageSize })
  2. Memoization:

    const users = useMemo(() => fetchedUsers, [fetchedUsers])
  3. 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

Last updated on