FluxUI Pro is live - modern UI, powerful animations, zero hassle.
Components
Table

Table

Ultra-modern data table component with sorting, filtering, pagination, selection, and advanced data management features.

Basic Table

Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024

Sortable Table

Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024
Alice Brownalice@example.comModeratorSupportactive1/13/2024
Charlie Wilsoncharlie@example.comUserEngineeringactive1/12/2024
Diana Princediana@example.comAdminHRinactive1/8/2024
Eve Garciaeve@example.comUserFinanceactive1/11/2024
Frank Millerfrank@example.comModeratorEngineeringactive1/9/2024

Filterable Table

Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024
Alice Brownalice@example.comModeratorSupportactive1/13/2024
Charlie Wilsoncharlie@example.comUserEngineeringactive1/12/2024
Diana Princediana@example.comAdminHRinactive1/8/2024
Eve Garciaeve@example.comUserFinanceactive1/11/2024
Frank Millerfrank@example.comModeratorEngineeringactive1/9/2024

Selectable Table

Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024
Alice Brownalice@example.comModeratorSupportactive1/13/2024
Charlie Wilsoncharlie@example.comUserEngineeringactive1/12/2024
Diana Princediana@example.comAdminHRinactive1/8/2024
Eve Garciaeve@example.comUserFinanceactive1/11/2024
Frank Millerfrank@example.comModeratorEngineeringactive1/9/2024

Paginated Table

Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024
Showing 1 to 3 of 8 entries

Products Table (Bordered)

Product
Category
Price
Stock
Rating
Status
Wireless HeadphonesElectronics$199.9945
4.5
In Stock
Smart WatchWearables$299.9923
4.2
In Stock
Laptop StandAccessories$49.990
4
Out of Stock
USB-C CableAccessories$19.99156
4.8
In Stock
Bluetooth SpeakerElectronics$79.9912
4.3
Low Stock

Compact Table

Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024
Alice Brownalice@example.comModeratorSupportactive1/13/2024

Loading State

Name
Email
Role
Department
Status
Last Login
Loading...

Empty State

Name
Email
Role
Department
Status
Last Login
No users found. Try adjusting your search criteria.

Advanced Features Demo

Complete User Management System

This table demonstrates all features working together: sorting, filtering, selection, pagination, and custom rendering.

Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024
Alice Brownalice@example.comModeratorSupportactive1/13/2024
Charlie Wilsoncharlie@example.comUserEngineeringactive1/12/2024
Showing 1 to 5 of 8 entries

Keyboard Navigation

Try these keyboard shortcuts:

  • Tab: Move focus between table elements
  • Space: Select/deselect rows (when selectable)
  • Enter: Trigger row click action
  • Arrow Keys: Navigate through sortable columns
  • Ctrl+A: Select all rows (when selectable)
Name
Email
Role
Department
Status
Last Login
John Doejohn@example.comAdminEngineeringactive1/15/2024
Jane Smithjane@example.comUserMarketingactive1/14/2024
Bob Johnsonbob@example.comUserSalesinactive1/10/2024

Installation

1

Install the packages

npm i motion clsx tailwind-merge lucide-react
2

Add util file

lib/util.ts
import { ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
3

Copy and paste the following code into your project

table.tsx
"use client"; import { motion, AnimatePresence } from "motion/react"; import React, { useState, useMemo } from "react"; import { cn } from "@/lib/utils"; import { ChevronUp, ChevronDown, ChevronsUpDown, Search, MoreHorizontal, Loader2, Check, Minus } from "lucide-react"; interface Column<T> { header: string; accessor: keyof T | string; sortable?: boolean; filterable?: boolean; width?: string; render?: (value: any, row: T, index: number) => React.ReactNode; className?: string; } interface PaginationConfig { pageSize?: number; showSizeChanger?: boolean; showQuickJumper?: boolean; showTotal?: (total: number, range: [number, number]) => string; } interface TableProps<T> { data: T[]; columns: Column<T>[]; loading?: boolean; sortable?: boolean; filterable?: boolean; selectable?: boolean; pagination?: boolean | PaginationConfig; striped?: boolean; bordered?: boolean; compact?: boolean; emptyMessage?: string; onRowClick?: (row: T, index: number) => void; onSelectionChange?: (selectedRows: T[]) => void; onSort?: (column: string, direction: 'asc' | 'desc') => void; className?: string; } function Table<T extends Record<string, any>>({ data, columns, loading = false, sortable = true, filterable = false, selectable = false, pagination = false, striped = false, bordered = false, compact = false, emptyMessage = "No data available", onRowClick, onSelectionChange, onSort, className, }: TableProps<T>) { const [sortColumn, setSortColumn] = useState<string | null>(null); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [searchTerm, setSearchTerm] = useState(''); const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set()); const [currentPage, setCurrentPage] = useState(1); const pageSize = typeof pagination === 'object' ? pagination.pageSize || 10 : 10; const showPagination = Boolean(pagination); // Filter and sort data const processedData = useMemo(() => { let filtered = data; // Apply search filter if (searchTerm && filterable) { filtered = data.filter(row => columns.some(col => { const value = row[col.accessor as keyof T]; return String(value).toLowerCase().includes(searchTerm.toLowerCase()); }) ); } // Apply sorting if (sortColumn && sortable) { filtered = [...filtered].sort((a, b) => { const aValue = a[sortColumn as keyof T]; const bValue = b[sortColumn as keyof T]; if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1; if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1; return 0; }); } return filtered; }, [data, searchTerm, sortColumn, sortDirection, columns, filterable, sortable]); // Paginate data const paginatedData = useMemo(() => { if (!showPagination) return processedData; const startIndex = (currentPage - 1) * pageSize; return processedData.slice(startIndex, startIndex + pageSize); }, [processedData, currentPage, pageSize, showPagination]); const totalPages = Math.ceil(processedData.length / pageSize); // Handle sorting const handleSort = (column: string) => { if (!sortable) return; if (sortColumn === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortColumn(column); setSortDirection('asc'); } onSort?.(column, sortDirection === 'asc' ? 'desc' : 'asc'); }; // Handle row selection const handleRowSelect = (index: number, checked: boolean) => { const newSelected = new Set(selectedRows); if (checked) { newSelected.add(index); } else { newSelected.delete(index); } setSelectedRows(newSelected); onSelectionChange?.(Array.from(newSelected).map(i => paginatedData[i])); }; // Handle select all const handleSelectAll = (checked: boolean) => { if (checked) { const allIndices = paginatedData.map((_, index) => index); setSelectedRows(new Set(allIndices)); onSelectionChange?.(paginatedData); } else { setSelectedRows(new Set()); onSelectionChange?.([]); } }; // Check if all rows are selected const allSelected = paginatedData.length > 0 && selectedRows.size === paginatedData.length; const someSelected = selectedRows.size > 0 && selectedRows.size < paginatedData.length; return ( <div className={cn("w-full", className)}> {/* Search */} {filterable && ( <motion.div className="mb-4 relative" initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} > <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <input type="text" placeholder="Search..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:border-gray-600 dark:text-white" /> </motion.div> )} {/* Table */} <motion.div className="overflow-hidden border border-gray-200 rounded-lg dark:border-gray-700" initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.3 }} > <div className="overflow-x-auto"> <table className="w-full"> {/* Table Header */} <thead className="bg-gray-50 dark:bg-gray-800"> <tr> {/* Selection Column */} {selectable && ( <th className="px-4 py-3 text-left"> <div className="flex items-center"> <input type="checkbox" checked={allSelected} ref={(el) => { if (el) el.indeterminate = someSelected; }} onChange={(e) => handleSelectAll(e.target.checked)} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600" /> </div> </th> )} {/* Data Columns */} {columns.map((column, index) => ( <th key={index} className={cn( "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", column.sortable && sortable && "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700", column.className )} style={{ width: column.width }} onClick={() => column.sortable && handleSort(column.accessor as string)} > <div className="flex items-center gap-2"> <span>{column.header}</span> {/* Sort Indicator */} {column.sortable && sortable && ( <motion.div animate={{ rotate: sortColumn === column.accessor ? sortDirection === 'asc' ? 0 : 180 : 0 }} transition={{ duration: 0.2 }} > {sortColumn === column.accessor ? ( sortDirection === 'asc' ? ( <ChevronUp className="w-4 h-4" /> ) : ( <ChevronDown className="w-4 h-4" /> ) ) : ( <ChevronsUpDown className="w-4 h-4 opacity-50" /> )} </motion.div> )} </div> </th> ))} </tr> </thead> {/* Table Body */} <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700"> {/* Loading State */} {loading && ( <tr> <td colSpan={columns.length + (selectable ? 1 : 0)} className="px-4 py-8 text-center" > <div className="flex items-center justify-center"> <Loader2 className="w-6 h-6 animate-spin text-blue-500" /> <span className="ml-2 text-gray-500">Loading...</span> </div> </td> </tr> )} {/* Empty State */} {!loading && paginatedData.length === 0 && ( <tr> <td colSpan={columns.length + (selectable ? 1 : 0)} className="px-4 py-8 text-center text-gray-500" > {emptyMessage} </td> </tr> )} {/* Data Rows */} {!loading && paginatedData.map((row, index) => { const actualIndex = (currentPage - 1) * pageSize + index; const isSelected = selectedRows.has(index); return ( <motion.tr key={actualIndex} className={cn( "hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-200", striped && index % 2 === 1 && "bg-gray-50 dark:bg-gray-800", isSelected && "bg-blue-50 dark:bg-blue-950", onRowClick && "cursor-pointer" )} onClick={() => onRowClick?.(row, actualIndex)} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2, delay: index * 0.05 }} > {/* Selection Column */} {selectable && ( <td className="px-4 py-3"> <input type="checkbox" checked={isSelected} onChange={(e) => handleRowSelect(index, e.target.checked)} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600" onClick={(e) => e.stopPropagation()} /> </td> )} {/* Data Columns */} {columns.map((column, colIndex) => { const value = row[column.accessor as keyof T]; const renderedValue = column.render ? column.render(value, row, actualIndex) : value; return ( <td key={colIndex} className={cn( "px-4 py-3 text-sm text-gray-900 dark:text-white", compact ? "py-2" : "py-3", bordered && "border border-gray-200 dark:border-gray-700", column.className )} > {renderedValue} </td> ); })} </motion.tr> ); })} </tbody> </table> </div> </motion.div> {/* Pagination */} {showPagination && totalPages > 1 && ( <motion.div className="flex items-center justify-between mt-4" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: 0.2 }} > <div className="text-sm text-gray-700 dark:text-gray-300"> Showing {(currentPage - 1) * pageSize + 1} to {Math.min(currentPage * pageSize, processedData.length)} of {processedData.length} entries </div> <div className="flex items-center gap-2"> <button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))} disabled={currentPage === 1} className="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800" > Previous </button> {/* Page Numbers */} {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { const pageNum = Math.max(1, Math.min(totalPages - 4, currentPage - 2)) + i; if (pageNum > totalPages) return null; return ( <button key={pageNum} onClick={() => setCurrentPage(pageNum)} className={cn( "px-3 py-1 text-sm border rounded", pageNum === currentPage ? "bg-blue-500 text-white border-blue-500" : "border-gray-300 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800" )} > {pageNum} </button> ); })} <button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))} disabled={currentPage === totalPages} className="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800" > Next </button> </div> </motion.div> )} {/* Selection Summary */} {selectable && selectedRows.size > 0 && ( <motion.div className="mt-4 p-3 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800" initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: 0.2 }} > <div className="flex items-center justify-between"> <span className="text-sm text-blue-700 dark:text-blue-300"> {selectedRows.size} row{selectedRows.size !== 1 ? 's' : ''} selected </span> <button onClick={() => { setSelectedRows(new Set()); onSelectionChange?.([]); }} className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" > Clear selection </button> </div> </motion.div> )} </div> ); } export default Table;
4

Update the import paths to match your project setup

Props

PropTypeDefaultDescription
dataT[][]Array of data objects to display in the table.
columnsColumn<T>[][]Column definitions with headers, accessors, and render functions.
loadingbooleanfalseShow loading state with skeleton rows.
sortablebooleantrueEnable column sorting functionality.
filterablebooleanfalseEnable global search/filter functionality.
selectablebooleanfalseEnable row selection with checkboxes.
paginationboolean | PaginationConfigfalseEnable pagination with customizable options.
stripedbooleanfalseAlternate row colors for better readability.
borderedbooleanfalseAdd borders to table cells.
compactbooleanfalseUse compact row spacing.
emptyMessagestring'No data available'Message to display when no data is available.
onRowClick(row: T, index: number) => voidundefinedCallback function when a row is clicked.
onSelectionChange(selectedRows: T[]) => voidundefinedCallback function when row selection changes.
onSort(column: string, direction: 'asc' | 'desc') => voidundefinedCallback function when sorting changes.