DataTable
A feature-rich data table component that provides advanced functionality for displaying and interacting with tabular data. Includes built-in support for sorting, filtering, pagination, row selection, custom actions, and responsive design.
Installation
pnpm add @helgadigitals/vera-ui lucide-reactnpm install @helgadigitals/vera-ui lucide-reactyarn add @helgadigitals/vera-ui lucide-reactBasic Usage
| name | role | status | |
|---|---|---|---|
| John Doe | john@example.com | Admin | Active |
| Jane Smith | jane@example.com | User | Inactive |
| Bob Johnson | bob@example.com | Editor | Active |
| Alice Brown | alice@example.com | User | Active |
| Charlie Wilson | charlie@example.com | Editor | Inactive |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function BasicDataTableExample() { return ( <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status']} /> );}Features
Sorting
Enable sorting on specific columns:
| name | role | status | lastLogin | |
|---|---|---|---|---|
| Alice Brown | alice@example.com | User | Active | 2024-01-14 |
| Bob Johnson | bob@example.com | Editor | Active | 2024-01-12 |
| Charlie Wilson | charlie@example.com | Editor | Inactive | 2024-01-08 |
| Jane Smith | jane@example.com | User | Inactive | 2024-01-10 |
| John Doe | john@example.com | Admin | Active | 2024-01-15 |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function SortableDataTableExample() { return ( <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status', 'lastLogin']} sortableColumns={['name', 'email', 'role', 'lastLogin']} multiSort={true} defaultSortState={[{ key: 'name', direction: 'asc' }]} /> );}Row Expansion
Enable expansion to display specific information about that row
| name | role | status | lastLogin | ||
|---|---|---|---|---|---|
| John Doe | john@example.com | Admin | Active | 2024-01-15 | |
| Jane Smith | jane@example.com | User | Inactive | 2024-01-10 | |
| Bob Johnson | bob@example.com | Editor | Active | 2024-01-12 | |
| Alice Brown | alice@example.com | User | Active | 2024-01-14 | |
| Charlie Wilson | charlie@example.com | Editor | Inactive | 2024-01-08 |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function RowExpansionDataTableExample() { return ( <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status', 'lastLogin']} expandable={true} expandOnRowClick={false} expandIcon={ <Plus className="w-3 h-3 hover:bg-white hover:text-black transition-colors" strokeWidth={4} /> } collapseIcon={ <Minus className="w-3 h-3 hover:bg-white hover:text-black transition-colors" strokeWidth={4} /> } expandedContent={(row) => ( <div className="p-4 bg-white dark:bg-gray-800 rounded border dark:border-gray-600"> <h4 className="font-semibold mb-2 dark:text-white">Users Details</h4> <div className="grid grid-cols-2 gap-4 text-sm dark:text-gray-200"> <div> <strong className="dark:text-white">Full Name:</strong> {row.name} </div> <div> <strong className="dark:text-white">Role:</strong> {row.role} </div> <div> <strong className="dark:text-white">Status:</strong> {row.status} </div> <div> <strong className="dark:text-white">Last Login:</strong> {row.lastLogin} </div> <div> <strong className="dark:text-white">Email:</strong> {row.email} </div> <div> <strong className="dark:text-white">Department:</strong> {row.department} </div> </div> </div> )} /> );}Global Search
Add a global search filter:
| name | role | status | department | |
|---|---|---|---|---|
| John Doe | john@example.com | Admin | Active | Engineering |
| Jane Smith | jane@example.com | User | Inactive | Marketing |
| Bob Johnson | bob@example.com | Editor | Active | Content |
| Alice Brown | alice@example.com | User | Active | Sales |
| Charlie Wilson | charlie@example.com | Editor | Inactive | Support |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function SearchableDataTableExample() { return ( <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status', 'department']} enableGlobalFilter={true} globalFilterPlaceholder="Search users..." sortableColumns={['name', 'email', 'role']} /> );}Row Selection
Enable single or multiple row selection:
| name | role | status | ||
|---|---|---|---|---|
| John Doe | john@example.com | Admin | Active | |
| Jane Smith | jane@example.com | User | Inactive | |
| Bob Johnson | bob@example.com | Editor | Active | |
| Alice Brown | alice@example.com | User | Active | |
| Charlie Wilson | charlie@example.com | Editor | Inactive |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function SelectableDataTableExample() { const [selectedRows, setSelectedRows] = useState<Array<string | number>>([]); return ( <div className="space-y-4"> {selectedRows.length > 0 && ( <div className="text-sm text-muted-foreground"> Selected {selectedRows.length} row(s): {selectedRows.join(', ')} </div> )} <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status']} selectable={true} selectionMode="multiple" selectedRowIds={selectedRows} onSelectionChange={setSelectedRows} maxSelections={3} onMaxSelectionsReached={(id) => { console.log(`Maximum selections reached. Attempted to select: ${id}`); }} /> </div> );}Custom Actions
Add action buttons for each row:
| name | role | status | Actions | |
|---|---|---|---|---|
| John Doe | john@example.com | Admin | Active | |
| Jane Smith | jane@example.com | User | Inactive | |
| Bob Johnson | bob@example.com | Editor | Active | |
| Alice Brown | alice@example.com | User | Active | |
| Charlie Wilson | charlie@example.com | Editor | Inactive |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function ActionsDataTableExample() { const actions = useMemo(() => [ { id: "view", label: "View", icon: <Eye className="h-4 w-4" />, onClick: (row: any) => console.log("View", row), }, { id: "edit", label: "Edit", icon: <Edit className="h-4 w-4" />, onClick: (row: any) => console.log("Edit", row), }, { id: "delete", label: "Delete", icon: <Trash2 className="h-4 w-4" />, onClick: (row: any) => console.log("Delete", row), variant: "destructive", disabled: (row: any) => row.role === "Admin", }, ], []); return ( <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status']} actions={actions} actionsPlacement="end" actionsColumnHeader="Actions" /> );}Custom Cell Rendering
Customize how data is displayed:
| name | role | Status | lastLogin | |
|---|---|---|---|---|
| John Doe | john@example.com | Admin | Active | 1/15/2024 |
| Jane Smith | jane@example.com | User | Inactive | 1/10/2024 |
| Bob Johnson | bob@example.com | Editor | Active | 1/12/2024 |
| Alice Brown | alice@example.com | User | Active | 1/14/2024 |
| Charlie Wilson | charlie@example.com | Editor | Inactive | 1/8/2024 |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function CustomRenderDataTableExample() { return ( <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status', 'lastLogin']} customBodyRender={(rowData, column) => { if (column === 'status') { return ( <Badge variant={rowData.status === 'Active' ? 'default' : 'secondary'}> {rowData.status} </Badge> ); } if (column === 'email') { return ( <a href={`mailto:${rowData.email}`} className="text-primary hover:underline" > {rowData.email} </a> ); } if (column === 'role') { return ( <Badge variant={rowData.role === 'Admin' ? 'destructive' : 'outline'}> {rowData.role} </Badge> ); } if (column === 'lastLogin') { return new Date(rowData.lastLogin).toLocaleDateString(); } return null; // Use default rendering }} customHeadRender={(column) => { if (column === 'status') { return <span className="font-bold text-primary">Status</span>; } return null; // Use default rendering }} sortableColumns={['name', 'email', 'lastLogin']} /> );}Loading and Empty States
Handle loading and empty data states:
| name | role | status | |
|---|---|---|---|
| John Doe | john@example.com | Admin | Active |
| Jane Smith | jane@example.com | User | Inactive |
| Bob Johnson | bob@example.com | Editor | Active |
| Alice Brown | alice@example.com | User | Active |
| Charlie Wilson | charlie@example.com | Editor | Inactive |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function BasicDataTableExample() { return ( <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status']} /> );}Products Example
Example with different data types and formatting:
| name | category | price | stock | status | Actions |
|---|---|---|---|---|---|
| Laptop Pro | Electronics | $1299.00 | 45 units | In Stock | |
| Wireless Mouse | Accessories | $29.00 | 150 units | In Stock | |
| Gaming Keyboard | Accessories | $89.00 | 0 units | Out of Stock | |
| Monitor 4K | Electronics | $399.00 | 12 units | Low Stock |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function ProductsDataTableExample() { const actions = useMemo(() => [ { id: "edit", label: "Edit Product", icon: <Edit className="h-4 w-4" />, onClick: (row: any) => console.log("Edit product", row), }, { id: "delete", label: "Delete Product", icon: <Trash2 className="h-4 w-4" />, onClick: (row: any) => console.log("Delete product", row), variant: "destructive", disabled: (row: any) => row.stock > 0, }, ], []); return ( <DataTable tableData={sampleProducts} tableColumns={['name', 'category', 'price', 'stock', 'status']} actions={actions} enableGlobalFilter={true} globalFilterPlaceholder="Search products..." sortableColumns={['name', 'category', 'price', 'stock']} customBodyRender={(rowData, column) => { if (column === 'price') { return `$${rowData.price.toFixed(2)}`; } if (column === 'status') { let variant: "default" | "secondary" | "destructive" = "default"; if (rowData.status === 'Out of Stock') variant = "destructive"; else if (rowData.status === 'Low Stock') variant = "secondary"; return ( <Badge variant={variant}> {rowData.status} </Badge> ); } if (column === 'stock') { return ( <span className={rowData.stock === 0 ? 'text-destructive font-semibold' : ''}> {rowData.stock} units </span> ); } return null; }} /> );}Advanced Usage
Complete Feature Example
Users Management
| name | role | status | department | lastLogin | Actions | |||
|---|---|---|---|---|---|---|---|---|
| Alice Brown | alice@example.com | User | Active | Sales | 1/14/2024 | |||
| Bob Johnson | bob@example.com | Editor | Active | Content | 1/12/2024 | |||
| Charlie Wilson | charlie@example.com | Editor | Inactive | Support | 1/8/2024 | |||
| Jane Smith | jane@example.com | User | Inactive | Marketing | 1/10/2024 | |||
| John Doe | john@example.com | Admin | Active | Engineering | 1/15/2024 |
import { DataTable } from "@helgadigitals/vera-ui";import { Badge } from "@helgadigitals/vera-ui";import { Button } from "@helgadigitals/vera-ui";import { Edit, Trash2, Eye, Plus, Minus } from "lucide-react";import { useState, useMemo } from "react";function AdvancedDataTableExample() { const [selectedRows, setSelectedRows] = useState<Array<string | number>>([]); const [sortState, setSortState] = useState<Array<{ key: keyof typeof sampleUsers[0]; direction: "asc" | "desc" }>>([{ key: 'name', direction: 'asc' }]); const [globalFilter, setGlobalFilter] = useState(""); const actions = useMemo(() => [ { id: "view", label: "View Details", icon: <Eye className="h-4 w-4" />, onClick: (row: any) => console.log("View user", row), }, { id: "edit", label: "Edit User", icon: <Edit className="h-4 w-4" />, onClick: (row: any) => console.log("Edit user", row), }, { id: "delete", label: "Delete User", icon: <Trash2 className="h-4 w-4" />, onClick: (row: any) => console.log("Delete user", row), variant: "destructive", disabled: (row: any) => row.role === "Admin", hidden: (row: any) => row.status === "Inactive", }, ], []); const handleBulkAction = () => { console.log("Bulk action for:", selectedRows); alert(`Bulk action performed on ${selectedRows.length} users`); }; return ( <div className="space-y-4"> {/* Header with actions */} <div className="flex justify-between items-center"> <h3 className="text-lg font-semibold">Users Management</h3> <div className="flex gap-2"> {selectedRows.length > 0 && ( <Button onClick={handleBulkAction} variant="outline" size="sm"> Bulk Action ({selectedRows.length}) </Button> )} <Button size="sm"> <Plus className="h-4 w-4 mr-2" /> Add User </Button> </div> </div> {/* Advanced DataTable */} <DataTable tableData={sampleUsers} tableColumns={['name', 'email', 'role', 'status', 'department', 'lastLogin']} // Sorting sortableColumns={['name', 'email', 'role', 'lastLogin']} multiSort={true} sortState={sortState} onSortChange={setSortState} // Filtering enableGlobalFilter={true} globalFilter={globalFilter} onGlobalFilterChange={setGlobalFilter} globalFilterPlaceholder="Search users by name, email, or role..." // Selection selectable={true} selectionMode="multiple" selectedRowIds={selectedRows} onSelectionChange={setSelectedRows} maxSelections={3} // Actions actions={actions} actionsPlacement="end" actionsColumnHeader="Actions" //Row Expansion expandable={true} expandOnRowClick={false} expandIcon={ <Plus className="w-3 h-3 hover:bg-white hover:text-black transition-colors" strokeWidth={4} /> } collapseIcon={ <Minus className="w-3 h-3 hover:bg-white hover:text-black transition-colors" strokeWidth={4} /> } expandedContent={(row) => ( <div className="p-4 bg-white dark:bg-gray-800 rounded border dark:border-gray-600"> <h4 className="font-semibold mb-2 dark:text-white">Users Details</h4> <div className="grid grid-cols-2 gap-4 text-sm dark:text-gray-200"> <div> <strong className="dark:text-white">Full Name:</strong> {row.name} </div> <div> <strong className="dark:text-white">Role:</strong> {row.role} </div> <div> <strong className="dark:text-white">Status:</strong> {row.status} </div> <div> <strong className="dark:text-white">Last Login:</strong> {row.lastLogin} </div> <div> <strong className="dark:text-white">Email:</strong> {row.email} </div> <div> <strong className="dark:text-white">Department:</strong> {row.department} </div> </div> </div> )} // Custom rendering customBodyRender={(rowData, column) => { switch (column) { case 'status': return ( <Badge variant={rowData.status === 'Active' ? 'default' : 'secondary'} > {rowData.status} </Badge> ); case 'role': return ( <Badge variant={rowData.role === 'Admin' ? 'destructive' : 'outline'} > {rowData.role} </Badge> ); case 'email': return ( <a href={`mailto:${rowData.email}`} className="text-primary hover:underline" > {rowData.email} </a> ); case 'lastLogin': return new Date(rowData.lastLogin).toLocaleDateString(); default: return null; } }} // Row interactions onRowClick={(row) => console.log("Row clicked:", row)} rowClassName={(row) => row.status === 'Inactive' ? 'opacity-60' : '' } // Styling className="border rounded-lg" // Empty state emptyMessage="No users found matching your criteria" /> {selectedRows.length > 0 && ( <div className="text-sm text-muted-foreground mt-2"> Selected users: {selectedRows.join(', ')} </div> )} </div> );}Props Reference
Core Props
Prop
Type
Selection Props
Prop
Type
Sorting Props
Prop
Type
Filtering Props
Prop
Type
Actions Props
Prop
Type
Custom Rendering Props
Prop
Type
TableAction Interface
interface TableAction<T> {
id: string; // Unique action identifier
label?: string; // Action label (for tooltips)
icon?: ReactNode; // Action icon
onClick: (row: T) => void; // Click handler
variant?: string; // Button variant
disabled?: (row: T) => boolean; // Dynamic disable condition
hidden?: (row: T) => boolean; // Dynamic visibility condition
}Utility Functions
The DataTable component works with several utility functions:
import { transformToSelectOptions, splitStringByUnderscore } from "@helgadigitals/vera-ui";
// Transform data for select options
const options = transformToSelectOptions(users, 'name', 'id');
// Format column headers
const formattedHeader = splitStringByUnderscore('user_name'); // "user name"Best Practices
Performance: For large datasets (>1000 rows), consider implementing server-side pagination, sorting, and filtering.
Row Identity: Always provide a getRowId function or ensure your data has unique id fields for proper selection and updates.
Recommended Patterns
- Controlled vs Uncontrolled: Use controlled components for complex state management
- Custom Rendering: Use
customBodyRenderfor consistent formatting across your app - Action Patterns: Group related actions and use consistent iconography
- Loading States: Always provide loading and empty states for better UX
- Responsive Design: Consider mobile layouts for complex tables
Accessibility
- All interactive elements are keyboard accessible
- Screen reader support with proper ARIA labels
- High contrast mode compatibility
- Focus management for table navigation
Examples
See the complete examples in the Examples section for more advanced usage patterns and real-world implementations.
Collapsible
A lightweight wrapper around @helgadigitals/vera-ui collapsible that provides an accessible and flexible way to show and hide content in your application.
Pagination
Navigation component for dividing content across multiple pages with intuitive controls for users to navigate through large datasets efficiently.