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