A modern, comprehensive React data table component library built with TypeScript, Tailwind CSS, and TanStack Table. Designed for creating production-ready data tables with advanced features like sorting, filtering, pagination, column management, drag-and-drop, and more.
- 📚 Live Documentation - Complete component documentation
- 🎨 Interactive Storybook - Documentation in Storybook
- 🔧 Component Examples - See all variations and use cases
Tucutable is a feature-rich, highly customizable data table component built on top of TanStack Table. It provides a complete solution for displaying, managing, and interacting with tabular data in React applications.
- Type-Safe: Built with TypeScript for full type safety
- Highly Customizable: Extensive configuration options
- Performance Optimized: Uses React.memo, useMemo, and efficient rendering strategies
- State Persistence: Automatic state persistence using Zustand with localStorage
- Accessible: Built with accessibility in mind
- Theme Support: Dark and light theme support
- Responsive: Mobile-first design with scrollable tables
src/
├── components/
│ ├── DataTable/ # Main table components
│ │ ├── DataTable.tsx # Main wrapper component
│ │ ├── DataTableComponent/ # Core table implementation
│ │ ├── TableWrapper/ # Table container wrapper
│ │ ├── TableHeader/ # Column headers with actions
│ │ ├── TableRow/ # Table rows
│ │ ├── TableCell/ # Individual cells
│ │ ├── TableHead/ # Header section
│ │ └── StateTableHandler/ # Empty/error state handler
│ ├── Assets/ # Icon components
│ ├── Common/ # Shared components (Spinner, Tooltip, etc.)
│ ├── Pagination/ # Pagination controls
│ ├── ManualPagination/ # Manual pagination component
│ ├── RowSelection/ # Row selection component
│ └── Footer/ # Table footer
├── context/
│ ├── index.tsx # Main DataTable context provider
│ ├── DragDropTableContext.tsx # Drag & drop for columns
│ └── DragDropContentContext.tsx # Drag & drop for content
├── hooks/
│ ├── useDataTableStore.tsx # Zustand store for persistence
│ ├── useScrollableTable.tsx # Scroll management
│ ├── useColumns.ts # Column processing
│ ├── useGetCommonPinningStyles.tsx # Pinning styles
│ └── useComponentEventListener.tsx # Event handling
├── common/
│ ├── types/ # TypeScript type definitions
│ ├── helpers/ # Utility functions
│ └── constants.ts # Constants
└── assets/
└── css/
└── index.css # Tailwind configuration & styles
DataTable (Wrapper)
└── DataTableProvider (Context)
└── DataTableComponent
├── TableWrapper
│ ├── TableHead
│ │ └── TableHeader (for each column)
│ │ ├── ColumnSort
│ │ ├── ColumnPin
│ │ ├── ColumnVisibility
│ │ ├── ColumnSearcher
│ │ └── ColumnDraggable
│ └── TableRow (for each row)
│ └── TableCell (for each cell)
│ ├── RowActionsCell
│ ├── RowSelectionCell
│ └── ExpandedRowCell
├── SubComponentDataTable (optional)
├── StateTableHandler (empty/error states)
├── Pagination
└── Footer
Tucutable uses Tailwind CSS v4 with a custom theme configuration defined in assets/css/index.css. The theme uses CSS custom properties (CSS variables) for dynamic theming.
All colors are defined as CSS custom properties:
--color-table-primary: #2196f3
--color-table-secondary: #662dff
--color-table-primary-text: #ffffff
--color-table-secondary-text: #b3b3b3
--color-table-row-bg: #1a1c20
--color-table-header-bg: #2a2d31
/* ... and many more */The component uses custom Tailwind utility classes:
bg-table-row-bg- Default row backgroundbg-table-row-expanded-bg- Expanded row backgroundbg-table-row-hover- Row hover statebg-table-header-bg- Header backgroundbg-table-dragged-bg- Dragging state background
text-table-primary-text- Primary text colortext-table-secondary-text- Secondary text colortext-table-disabled- Disabled text color
border-table-divider- Divider border colorborder-table-divider-columns- Column divider color
table-sticky-header- Sticky header positioningtable-scrollable- Scrollable containertable-transition- Standard transitionstable-text-ellipsis- Text truncationtable-row-hover-shadow- Hover shadow effecttable-dropdown-shadow- Dropdown shadowtable-resizer- Column resizer styles
The component uses Tailwind's responsive utilities and custom breakpoints. Tables are scrollable on smaller screens with horizontal scrolling.
-
Data Display
- Render tabular data with customizable columns
- Support for nested data structures
- Custom cell renderers
- Virtual scrolling support
-
Sorting
- Single and multi-column sorting
- Custom sort functions
- Sort indicators (ascending/descending)
- Persistent sort state
-
Filtering
- Column-level filtering
- Global search
- Custom filter functions
- Filter state management
-
Pagination
- Client-side pagination
- Server-side pagination
- Manual pagination
- Customizable page sizes
- Page navigation controls
-
Column Management
- Column visibility toggle
- Column reordering (drag & drop)
- Column resizing
- Column pinning (left/right)
- Column hiding/showing
-
Row Features
- Row selection (checkbox/radio)
- Row actions menu
- Expandable rows
- Sub-components per row
- Nested tables
-
State Management
- Automatic state persistence (localStorage)
- Zustand store per table instance
- State restoration on mount
- Configurable persistence
-
Export Functionality
- Export cell values
- Export headers
- Numeric value parsing
- Percentage formatting
- Drag & Drop: Reorder columns via drag and drop using
@dnd-kit - Theming: Dark/light theme support with CSS variables
- Accessibility: ARIA labels, keyboard navigation
- Performance: Memoization, lazy loading, efficient re-renders
- Customization: Extensive prop system for styling and behavior
- Error Handling: Built-in error states and messages
- Loading States: Loading indicators and skeleton states
The DataTableProvider wraps the table and provides context to all child components.
import { DataTableProvider, useDataTableContext } from '@tucutable';
function MyTable() {
return (
<DataTableProvider
tableId="my-table"
data={data}
columns={columns}
// ... other props
>
<DataTableComponent data={data} />
</DataTableProvider>
);
}Access the table context from any child component:
import { useDataTableContext } from '@e-burgos/tucutable';
function MyComponent() {
const context = useDataTableContext();
if (!context) return null;
const {
tableState, // Current table state
actions, // Table actions
table, // TanStack Table instance
utils, // Utility flags
config, // Table configuration
scrollProps, // Scroll properties
tableContainerRef, // Container ref
} = context;
// Use the context...
}interface DataTableStore {
tableState: {
id: string;
pagination: PaginationState;
sorting: SortingState;
columnOrder: ColumnOrderState;
columnVisibility: VisibilityState;
columnPinning: ColumnPinningState;
columnFilters: ColumnFiltersState;
rowSelection?: RowSelectionState;
totalCount?: number;
reportData: ReportDataState;
};
actions: {
setTotalCount?: (value: number) => void;
resetStoreData: () => void;
setColumnFilters: (value: ColumnFiltersState) => void;
onSetReportCellValue: (value: string, rowId: string, cellIndex: number, options?: { hasSubTable?: boolean }) => void;
onSetReportHeader: (value: string, cellIndex: number) => void;
resetReportData: () => void;
};
utils: {
isEmpty: boolean;
checkState: boolean;
handleFetch: boolean;
isSubComponent: boolean;
isManualPagination: boolean;
isRowSelection: boolean;
};
table: Table<TData>;
config: Omit<DataTableProps, 'data'>;
scrollProps: UseScrollableTable;
tableContainerRef: MutableRefObject<HTMLDivElement>;
}Manages persistent state using Zustand:
import { useDataTableStore } from '@e-burgos/tucutable';
const {
pagination,
sorting,
columnOrder,
columnVisibility,
setPagination,
setSorting,
// ... other state and setters
} = useDataTableStore(tableId);Manages table scrolling:
import { useScrollableTable } from '@e-burgos/tucutable';
const scrollProps = useScrollableTable(tableContainerRef);
// Returns:
// {
// containerWith: number;
// isScrollable: boolean;
// scrollX: number;
// handleScroll: (e: React.UIEvent) => void;
// }Gets styles for pinned columns:
import { useGetCommonPinningStyles } from '@e-burgos/tucutable';
const { pinStyles, isPinned } = useGetCommonPinningStyles(column);Listens to component events:
import { useComponentEventListener } from '@e-burgos/tucutable';
const { element } = useComponentEventListener(`${tableId}-container`);Processes and transforms columns:
import { useColumns } from '@e-burgos/tucutable';
const processedColumns = useColumns({
columns: defaultColumns,
containerWidth,
offset,
isSubComponent,
isRowActions,
isRowSelection,
columnVisibility,
});<DataTable
sx={{
row: { backgroundColor: 'red' },
cell: { padding: '10px' },
wrapper: { border: '1px solid blue' },
}}
/>Override theme variables:
[data-theme='light'] {
--color-table-primary: #your-color;
--color-table-row-bg: #your-bg;
}Use custom utility classes:
<DataTable className="bg-table-paper-bg border-table-divider" />interface IDataTableStyles {
wrapper?: React.CSSProperties;
wrapperContainer?: React.CSSProperties;
tableContainer?: React.CSSProperties;
messageContainer?: React.CSSProperties;
table?: React.CSSProperties;
thead?: React.CSSProperties;
tbody?: React.CSSProperties;
tfoot?: React.CSSProperties;
header?: React.CSSProperties;
row?: React.CSSProperties;
rowExpanded?: React.CSSProperties;
cell?: React.CSSProperties;
pagination?: React.CSSProperties;
container?: React.CSSProperties;
}- Primary:
#2196f3(Blue) - Secondary:
#662dff(Purple)
- Primary Text:
#ffffff(White) - Secondary Text:
#b3b3b3(Light Gray) - Disabled:
#4d4d4d(Dark Gray)
- Paper BG:
#16191f - Default BG:
#101217 - Box BG:
#0a0b0d - Row BG:
#1a1c20 - Row Expanded BG:
#191b24 - Row Pinned:
#1d2025 - Row Hover:
#292b31 - Header BG:
#2a2d31 - Action BG:
#1b1d22 - Dragged BG:
#0a0b0d
- Divider:
#333333 - Column Divider:
#595959
- Primary Text:
#212121(Dark Gray) - Secondary Text:
#999999(Gray) - Disabled:
#9e9e9e(Medium Gray)
- Paper BG:
#ebeff7 - Default BG:
#f8fafc - Box BG:
#ffffff - Row BG:
#ffffff - Row Expanded BG:
#f8fafc - Row Pinned:
#f8fafc - Row Hover:
#f8fafc - Header BG:
#fafafa - Action BG:
#1b1d22 - Dragged BG:
#e5ebf7
- Divider:
#e0e0e0 - Column Divider:
#b8b8b8
// Via mode prop
<DataTable mode="light" ... />
// Via data attribute
<div data-theme="light">
<DataTable ... />
</div>
// Via class
<div className="light">
<DataTable ... />
</div>All icons are exported from @e-burgos/tucutable/components/Assets:
ArrowIndicator- Sort arrowArrowDoubleIndicator- Multi-sort indicatorArrowLongIndicator- Long arrowArrowPaginationIndicator- Pagination arrowsColumnIndicator- Column iconDeleteIndicator- Delete actionDownloadIndicator- Download actionDragIndicator- Drag handleEditIndicator- Edit actionFilterIndicator- Filter iconMoreIndicator- More options (three dots)OpenNewTab- Open in new tabPinIndicator- Pin/unpin columnRowIndicator- Row expand/collapseViewDetailsIndicator- View detailsVisibilityIndicator- Show/hide columnVoidIndicator- Void action
import {
ArrowIndicator,
PinIndicator,
DragIndicator
} from '@e-burgos/tucutable';
<ArrowIndicator />
<PinIndicator isPinned={true} />
<DragIndicator />Icons inherit color from their parent and use currentColor for fill/stroke. They respond to hover states and theme changes automatically.
animation: fadeInAnimation 0.3s ease-in-out;animation: slideTop 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);animation: slideOutTop 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53);animation: spin 1s linear infinite;Animations are applied automatically to:
- Row expansions/collapses
- Dropdown menus
- Loading spinners
- State transitions
You can add custom animations via CSS:
@keyframes myAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.my-component {
animation: myAnimation 0.3s ease-in-out;
}All interactive elements use smooth transitions:
transition: background-color 0.2s ease-in-out;transition: all 0.2s ease-in-out;transition: transform 0.2s ease-in-out;- Fast:
150ms(resizers, quick interactions) - Standard:
200ms(most UI elements) - Slow:
300ms(animations, complex transitions)
ease-in-out- Standard transitionscubic-bezier(0.25, 0.46, 0.45, 0.94)- Smooth animationslinear- Loading spinners
- Row hover states
- Cell background changes
- Column drag operations
- Button hover states
- Dropdown open/close
- Icon color changes
- Resizer interactions
- Hover: Background color change, shadow effect
- Click: Row selection, expand/collapse
- Drag: Row reordering (if enabled)
- Hover: Header highlight, resize handle appears
- Click: Sort toggle, pin toggle
- Drag: Column reordering
- Resize: Column width adjustment
- Hover: Background color change
- Click: Cell selection, action triggers
- Focus: Keyboard navigation
- Shows spinner
- Disables interactions
- Maintains layout
- Shows custom message
- Provides action buttons
- Maintains table structure
- Shows error message
- Provides retry option
- Maintains accessibility
- Shows sub-component
- Highlights expanded row
- Maintains scroll position
- Tab: Navigate between interactive elements
- Arrow Keys: Navigate cells (if enabled)
- Enter/Space: Activate selected element
- Escape: Close dropdowns, cancel actions
The component uses TypeScript for compile-time validation:
interface DataTableProps<TData> {
tableId: string; // Required
data: Array<TData>; // Required
columns: Array<ColumnDef<any, any>>; // Required
// ... other props with types
}tableId: Must be a non-empty stringdata: Must be an arraycolumns: Must be a non-empty array
- Column definitions are validated against TanStack Table requirements
- Pagination props are validated for consistency
- Row selection props are validated for type compatibility
Invalid configurations result in:
- Console warnings
- Fallback to default values
- Graceful degradation
Cause: Infinite loop in state updates
Solution: Fixed by deferring Zustand store updates with queueMicrotask()
Cause: State updates during render
Solution: All store updates are deferred to microtasks
Cause: Using context outside provider
Solution: Ensure component is wrapped in DataTableProvider
The component handles errors gracefully:
interface IDataTableStateMessage {
errorData?: string;
errorDataDescription?: string;
contactSupport?: string;
contactSupportLink?: string;
hideContactSupport?: boolean;
}Errors are displayed in the StateTableHandler component with:
- Error message
- Error description
- Support contact information
- Retry option (if applicable)
Warning: "Table ID is required"
Impact: State persistence won't work correctly
Warning: "Column definition is invalid"
Impact: Column may not render correctly
Warning: "Duplicate column ID detected"
Impact: Column behavior may be unpredictable
The component logs warnings for:
- Missing required props
- Invalid prop combinations
- Deprecated prop usage
- Performance issues
Warnings can be suppressed via ESLint comments, but it's recommended to fix the underlying issues.
-
Memoize Data: Use
useMemofor processed dataconst processedData = useMemo(() => processData(rawData), [rawData]);
-
Memoize Columns: Prevent unnecessary re-renders
const columns = useMemo(() => columnDefs, [dependencies]);
-
Virtual Scrolling: Enable for large datasets
<DataTable // Use virtual scrolling for > 1000 rows enableVirtualization={data.length > 1000} />
-
Lazy Loading: Load data incrementally
const { data, isLoading } = useInfiniteQuery(...);
-
Unique tableId: Always use unique IDs for multiple tables
<DataTable tableId={`table-${userId}-${tableType}`} />
-
Column Definitions: Define columns outside component or memoize
const columns = useMemo(() => [...], []);
-
State Management: Use context actions instead of direct state manipulation
const { actions } = useDataTableContext(); actions?.setTotalCount(count);
-
Styling: Use CSS variables for theming instead of inline styles
:root { --color-table-primary: #your-color; }
-
Accessibility: Always provide proper labels and ARIA attributes
<DataTable aria-label="User data table" // ... />
const columns: ColumnDef<Data>[] = [
{
id: 'actions',
cell: ({ row }) => {
const { actions } = useDataTableContext();
return <CustomActions row={row} actions={actions} />;
},
},
];const columns = useMemo(() => {
return baseColumns.filter((col) => {
if (col.id === 'sensitive' && !hasPermission) return false;
return true;
});
}, [hasPermission]);const { tableState, actions } = useDataTableContext();
useEffect(() => {
if (userPreferences.columnOrder) {
// Restore user's preferred column order
actions?.setColumnOrder(userPreferences.columnOrder);
}
}, []);const { data, isLoading } = useQuery({
queryKey: ['table-data', pageIndex, pageSize],
queryFn: () => fetchData(pageIndex, pageSize),
keepPreviousData: true,
});const { tableState } = useDataTableContext();
const exportToCSV = () => {
const { reportData } = tableState;
// Use reportData.rows and reportData.headers
// to generate CSV
};<DataTable
stateMessage={{
noData: 'No items found',
noDataDescription: 'Try adjusting your filters',
// Custom empty state component
}}
/>const rowActions: IRowActions<Data>[] = [
{
action: 'edit',
onClick: (row) => handleEdit(row),
showOptions: (row) => hasPermission('edit', row),
requiredScopes: ['edit'],
},
];<DataTable
renderSubDataTable={{
columns: subColumns,
data: (row) => row.subItems,
expandedColumnSize: 50,
}}
/>const getRowStyle = (row: Row<Data>) => {
if (row.original.isHighlighted) {
return { backgroundColor: 'yellow' };
}
return {};
};
<DataTable
sx={{
row: (row) => getRowStyle(row),
}}
/>;// Custom storage key
const tableId = `user-${userId}-table-${tableType}`;
<DataTable
tableId={tableId}
// State automatically persists to localStorage
// with key: `${tableId}-datatable`
/>;npm install @e-burgos/tucutable
# or
yarn add @e-burgos/tucutable
# or
pnpm add @e-burgos/tucutableimport { DataTable, TanstackTable } from '@e-burgos/tucutable';
interface User {
id: string;
name: string;
email: string;
}
const columns: TanstackTable.ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
header: 'Email',
},
];
function App() {
const data: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
];
return <DataTable tableId="users-table" data={data} columns={columns} />;
}See the full API documentation for complete prop definitions.
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
MIT License
For issues, questions, or feature requests, please open an issue on GitHub or contact support.