
Building Tables
- 59 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
Building-tables is a Claude Code skill that builds tables and data grids from simple HTML tables to enterprise data grids with sorting, filtering, pagination, and virtualization.
About
Building-tables is a Claude Code skill for building tables and data grids, from simple HTML tables to enterprise grids handling millions of rows. A developer uses it when implementing sorting, filtering, pagination, or handling large datasets. It provides a data-volume decision framework, performance-optimization strategies, WCAG/ARIA accessibility patterns, and library recommendations like TanStack Table and AG Grid.
- Data-volume decision framework from HTML tables to virtualized grids
- Sorting, filtering, pagination, selection, inline editing, and export
- Performance thresholds and TanStack Table / AG Grid recommendations
Building Tables by the numbers
- 59 all-time installs (skills.sh)
- Ranked #1,218 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
building-tables capabilities & compatibility
- Capabilities
- building tables · building forms · creating dashboards · building ai chat
- Use cases
- frontend · ui design · data analysis
What building-tables says it does
Builds tables and data grids for displaying tabular information, from simple HTML tables to complex enterprise data grids.
10,000-100,000 → Virtual scrolling with windowing
npx skills add https://github.com/ancoleman/ai-design-components --skill building-tablesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Build a data grid with sorting, filtering, pagination, and virtual scrolling scaled to dataset size.
Who is it for?
Building data grids with sorting, filtering, pagination, and virtual scrolling.
Skip if: Backend data querying or aggregation logic.
When should I use this skill?
Creating tables, data grids, or spreadsheet-like interfaces for tabular data.
What you get
A tiered table implementation matched to data volume with sorting, filtering, and export.
- Tiered table implementation
- Sorting/filtering/pagination
- Virtual scrolling for large datasets
By the numbers
- Data-volume framework spanning <100 rows to >100,000 rows
- Virtual scrolling: 10,000+ rows (60fps, constant memory)
Files
Building Tables & Data Grids
Purpose
This skill enables systematic creation of tables and data grids from simple HTML tables to enterprise-scale virtualized grids handling millions of rows. It provides clear decision frameworks based on data volume and required features, ensuring optimal performance, accessibility, and responsive design across all implementations.
When to Use
Activate this skill when:
- Creating tables, data grids, or spreadsheet-like interfaces
- Displaying tabular or structured data
- Implementing sorting, filtering, or pagination features
- Handling large datasets or addressing performance concerns
- Building inline editing or data entry interfaces
- Requiring row selection or bulk operations
- Implementing data export (CSV, Excel, PDF)
- Ensuring table accessibility or responsive behavior
Quick Decision Framework
Select implementation tier based on data volume:
<100 rows → Simple HTML table with progressive enhancement
100-1,000 rows → Client-side features (sort, filter, paginate)
1,000-10,000 → Server-side operations with API pagination
10,000-100,000 → Virtual scrolling with windowing
>100,000 rows → Enterprise grid with streaming and workersFor detailed selection criteria, reference references/selection-framework.md.
Core Implementation Patterns
Tier 1: Basic Tables (<100 rows)
For simple, read-only data display:
- Use semantic HTML
<table>structure - Add responsive behavior via CSS
- Implement client-side sorting if needed
- Reference
references/basic-tables.mdfor patterns
Example: examples/simple-responsive-table.tsx
Tier 2: Interactive Tables (100-10K rows)
For feature-rich interactions:
- Add filtering, pagination, and selection
- Implement inline or modal editing
- Use client-side operations up to 1K rows
- Switch to server-side beyond 1K rows
- Reference
references/interactive-tables.md
Example: examples/sortable-filtered-table.tsx
Tier 3: Advanced Grids (10K+ rows)
For massive datasets:
- Implement virtual scrolling
- Use server-side aggregation
- Add grouping and hierarchies
- Consider enterprise solutions
- Reference
references/advanced-grids.md
Example: examples/virtual-scrolling-grid.tsx
Performance Optimization
Critical performance thresholds:
- Client-side operations: <1,000 rows (instant, <50ms)
- Server-side operations: 1,000-10,000 rows (<200ms API)
- Virtual scrolling: 10,000+ rows (60fps, constant memory)
- Streaming: 100,000+ rows (progressive rendering)
To benchmark performance:
# Generate test data
python scripts/generate_mock_data.py --rows 10000
# Analyze rendering performance
node scripts/analyze_performance.jsFor optimization strategies, reference references/performance-optimization.md.
Feature Implementation
Sorting
- Single or multi-column sorting
- Custom sort logic (numeric, date, natural)
- Visual indicators and keyboard support
- Reference
references/sorting-filtering.md
Filtering & Search
- Column-specific filters (text, range, select)
- Global search across all columns
- Advanced filter logic (AND/OR)
- Reference
references/sorting-filtering.md
Pagination
- Client-side for small datasets
- Server-side for large datasets
- Infinite scroll alternative
- Reference
references/pagination-strategies.md
Selection & Bulk Actions
- Single or multi-row selection
- Range selection (Shift+click)
- Bulk operations toolbar
- Reference
references/selection-patterns.md
Inline Editing
- Cell-level or row-level editing
- Validation and error handling
- Optimistic updates
- Reference
references/editing-patterns.md
Export
- CSV, Excel, PDF formats
- Preserve formatting and encoding
- Stream large exports
- Run
scripts/export_table_data.py
Accessibility Requirements
Essential WCAG compliance:
- Semantic HTML with proper structure
- ARIA grid pattern for interactive tables
- Full keyboard navigation
- Screen reader announcements
To validate accessibility:
node scripts/validate_accessibility.jsFor complete requirements, reference references/accessibility-patterns.md.
Responsive Design
Four proven strategies: 1. Horizontal scroll - Simple, preserves structure 2. Card stack - Transform rows to cards on mobile 3. Priority columns - Hide less important columns 4. Truncate & expand - Compact with details on demand
See examples/responsive-patterns.tsx for implementations. Reference references/responsive-strategies.md for details.
Library Recommendations
Primary: TanStack Table (Headless)
Best for custom designs and complete control:
- TypeScript-first with excellent DX
- Small bundle size (~15KB)
- Framework agnostic
- Virtual scrolling support
npm install @tanstack/react-tableSee examples/tanstack-basic.tsx for setup.
Enterprise: AG Grid
Best for feature-complete solutions:
- Handles millions of rows
- Built-in advanced features
- Community (free) + Enterprise (paid)
- Excel-like user experience
npm install ag-grid-reactSee examples/ag-grid-enterprise.tsx for setup.
For detailed comparison, reference references/library-comparison.md.
Design Token Integration
Tables use the design-tokens skill for consistent theming:
- Color tokens for backgrounds, borders, and states
- Spacing tokens for cell padding
- Typography tokens for text styling
- Shadow tokens for elevation
Supports light, dark, high-contrast, and custom themes. Reference the design-tokens skill for theme switching.
Working Examples
Start with the example matching the requirements:
simple-responsive-table.tsx # Basic HTML table
sortable-filtered-table.tsx # With sorting and filtering
paginated-server-table.tsx # Server-side pagination
virtual-scrolling-grid.tsx # High-performance for 100K+ rows
editable-data-grid.tsx # Inline editing with validation
grouped-aggregated-table.tsx # Hierarchical with aggregationsTesting Tools
Generate test data:
python scripts/generate_mock_data.py --rows 100000 --columns 20Benchmark performance:
node scripts/analyze_performance.js --rows 10000Validate accessibility:
node scripts/validate_accessibility.jsNext Steps
1. Determine the data volume and feature requirements 2. Select the appropriate implementation tier 3. Choose between TanStack Table (flexibility) or AG Grid (features) 4. Start with the matching example file 5. Implement core features progressively 6. Test performance and accessibility 7. Apply responsive strategy for mobile
id,name,email,department,role,location,salary,start_date,is_active,performance_rating
1,Alice Johnson,alice.johnson@example.com,Engineering,Senior Developer,San Francisco,125000,2021-03-15,true,4.5
2,Bob Smith,bob.smith@example.com,Sales,Account Manager,New York,95000,2020-07-22,true,4.2
3,Charlie Brown,charlie.brown@example.com,Marketing,Marketing Specialist,Chicago,72000,2022-01-10,true,3.8
4,Diana Prince,diana.prince@example.com,Engineering,Tech Lead,San Francisco,145000,2019-11-05,true,4.8
5,Edward Norton,edward.norton@example.com,HR,HR Manager,Boston,88000,2021-09-20,true,4.0
6,Fiona Green,fiona.green@example.com,Finance,Financial Analyst,New York,92000,2020-05-18,true,4.3
7,George Williams,george.williams@example.com,Engineering,Junior Developer,Austin,85000,2023-02-28,true,3.5
8,Helen Martinez,helen.martinez@example.com,Product,Product Manager,Seattle,135000,2019-08-12,true,4.6
9,Ian Thompson,ian.thompson@example.com,Sales,Sales Director,Los Angeles,155000,2018-04-03,true,4.7
10,Julia Roberts,julia.roberts@example.com,Design,UI/UX Designer,Portland,98000,2021-06-14,true,4.4
11,Kevin Davis,kevin.davis@example.com,Engineering,DevOps Engineer,Denver,115000,2020-10-25,true,4.1
12,Lisa Anderson,lisa.anderson@example.com,Marketing,Content Manager,Chicago,78000,2022-03-07,true,3.9
13,Michael Chen,michael.chen@example.com,Engineering,Senior Developer,San Francisco,132000,2019-12-01,true,4.5
14,Nancy Wilson,nancy.wilson@example.com,HR,Recruiter,Boston,68000,2023-01-15,true,3.6
15,Oliver Taylor,oliver.taylor@example.com,Finance,CFO,New York,225000,2017-09-10,true,4.9
16,Patricia Moore,patricia.moore@example.com,Legal,Legal Counsel,Washington DC,165000,2018-11-20,true,4.4
17,Quinn Jackson,quinn.jackson@example.com,Engineering,QA Engineer,Austin,95000,2021-04-05,true,4.0
18,Rachel White,rachel.white@example.com,Product,Senior Product Manager,Seattle,148000,2019-07-18,true,4.7
19,Samuel Harris,samuel.harris@example.com,Sales,Sales Representative,Miami,72000,2022-08-22,true,3.7
20,Tina Martin,tina.martin@example.com,Customer Support,Support Manager,Phoenix,82000,2020-12-10,true,4.2
21,Uma Patel,uma.patel@example.com,Engineering,Data Scientist,San Francisco,142000,2019-05-30,false,4.6
22,Victor Lee,victor.lee@example.com,Marketing,Marketing Director,New York,175000,2018-03-25,true,4.8
23,Wendy Clark,wendy.clark@example.com,Design,Design Director,Portland,138000,2019-10-15,true,4.5
24,Xavier Rodriguez,xavier.rodriguez@example.com,Engineering,Backend Developer,Austin,108000,2021-02-20,true,4.1
25,Yolanda King,yolanda.king@example.com,HR,Benefits Specialist,Boston,75000,2022-06-08,true,3.8{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Table Configuration Schema",
"description": "Schema for configuring table components with features, columns, and behavior",
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the table instance"
},
"columns": {
"type": "array",
"description": "Column definitions for the table",
"items": {
"type": "object",
"required": ["id", "header"],
"properties": {
"id": {
"type": "string",
"description": "Unique column identifier"
},
"header": {
"type": "string",
"description": "Column header text"
},
"accessor": {
"type": "string",
"description": "Property path to access cell data"
},
"width": {
"type": ["number", "string"],
"description": "Column width (pixels or percentage)"
},
"minWidth": {
"type": "number",
"description": "Minimum column width in pixels"
},
"maxWidth": {
"type": "number",
"description": "Maximum column width in pixels"
},
"sortable": {
"type": "boolean",
"default": true,
"description": "Enable sorting for this column"
},
"filterable": {
"type": "boolean",
"default": true,
"description": "Enable filtering for this column"
},
"resizable": {
"type": "boolean",
"default": true,
"description": "Allow column resizing"
},
"editable": {
"type": "boolean",
"default": false,
"description": "Enable inline editing"
},
"type": {
"type": "string",
"enum": ["string", "number", "date", "boolean", "select", "currency", "percentage"],
"default": "string",
"description": "Data type for the column"
},
"format": {
"type": "string",
"description": "Format string for display (e.g., date format, number format)"
},
"align": {
"type": "string",
"enum": ["left", "center", "right"],
"default": "left",
"description": "Text alignment in cells"
},
"hidden": {
"type": "boolean",
"default": false,
"description": "Hide column by default"
},
"sticky": {
"type": "string",
"enum": ["left", "right"],
"description": "Make column sticky when scrolling"
},
"cell": {
"type": "object",
"description": "Custom cell rendering options",
"properties": {
"component": {
"type": "string",
"description": "Custom component name for rendering"
},
"props": {
"type": "object",
"description": "Props to pass to custom component"
}
}
},
"filter": {
"type": "object",
"description": "Column filter configuration",
"properties": {
"type": {
"type": "string",
"enum": ["text", "select", "multiSelect", "range", "date", "dateRange", "boolean"],
"description": "Filter input type"
},
"options": {
"type": "array",
"description": "Options for select filters",
"items": {
"type": "object",
"properties": {
"label": {"type": "string"},
"value": {"type": ["string", "number", "boolean"]}
}
}
},
"placeholder": {
"type": "string",
"description": "Filter input placeholder"
}
}
},
"validation": {
"type": "object",
"description": "Validation rules for editable columns",
"properties": {
"required": {
"type": "boolean",
"description": "Field is required"
},
"min": {
"type": "number",
"description": "Minimum value (for numbers) or length (for strings)"
},
"max": {
"type": "number",
"description": "Maximum value (for numbers) or length (for strings)"
},
"pattern": {
"type": "string",
"description": "Regex pattern for validation"
},
"custom": {
"type": "string",
"description": "Custom validation function name"
}
}
}
}
}
},
"features": {
"type": "object",
"description": "Table feature configuration",
"properties": {
"sorting": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"multiSort": {
"type": "boolean",
"default": false,
"description": "Allow sorting by multiple columns"
},
"defaultSort": {
"type": "array",
"description": "Default sort configuration",
"items": {
"type": "object",
"properties": {
"column": {"type": "string"},
"direction": {
"type": "string",
"enum": ["asc", "desc"]
}
}
}
}
}
},
"filtering": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"globalSearch": {
"type": "boolean",
"default": false,
"description": "Enable global search across all columns"
},
"debounceMs": {
"type": "number",
"default": 300,
"description": "Debounce delay for filter inputs"
}
}
},
"pagination": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"type": {
"type": "string",
"enum": ["client", "server", "infinite"],
"default": "client"
},
"pageSize": {
"type": "number",
"default": 25
},
"pageSizeOptions": {
"type": "array",
"default": [10, 25, 50, 100],
"items": {
"type": "number"
}
},
"showPageInfo": {
"type": "boolean",
"default": true
}
}
},
"selection": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"type": {
"type": "string",
"enum": ["single", "multi"],
"default": "multi"
},
"showCheckboxes": {
"type": "boolean",
"default": true
},
"selectOnRowClick": {
"type": "boolean",
"default": false
}
}
},
"editing": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"type": {
"type": "string",
"enum": ["inline", "row", "modal"],
"default": "inline"
},
"validation": {
"type": "boolean",
"default": true
},
"confirmSave": {
"type": "boolean",
"default": false
}
}
},
"export": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"formats": {
"type": "array",
"items": {
"type": "string",
"enum": ["csv", "excel", "pdf", "json"]
},
"default": ["csv"]
}
}
},
"grouping": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"defaultGroups": {
"type": "array",
"items": {
"type": "string"
}
},
"showAggregations": {
"type": "boolean",
"default": true
}
}
},
"virtualization": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false,
"description": "Enable virtual scrolling for large datasets"
},
"rowHeight": {
"type": "number",
"default": 40,
"description": "Fixed row height for virtual scrolling"
},
"overscan": {
"type": "number",
"default": 5,
"description": "Number of rows to render outside viewport"
}
}
}
}
},
"appearance": {
"type": "object",
"description": "Visual appearance configuration",
"properties": {
"theme": {
"type": "string",
"enum": ["default", "compact", "comfortable", "dark"],
"default": "default"
},
"striped": {
"type": "boolean",
"default": false,
"description": "Alternate row colors"
},
"bordered": {
"type": "boolean",
"default": true,
"description": "Show cell borders"
},
"hover": {
"type": "boolean",
"default": true,
"description": "Highlight rows on hover"
},
"stickyHeader": {
"type": "boolean",
"default": false,
"description": "Keep header visible when scrolling"
},
"height": {
"type": ["number", "string"],
"description": "Table container height"
},
"maxHeight": {
"type": ["number", "string"],
"description": "Maximum table height before scrolling"
}
}
},
"responsive": {
"type": "object",
"description": "Responsive behavior configuration",
"properties": {
"breakpoint": {
"type": "string",
"enum": ["sm", "md", "lg", "xl"],
"default": "md",
"description": "Breakpoint for responsive behavior"
},
"mode": {
"type": "string",
"enum": ["scroll", "stack", "priority", "collapse"],
"default": "scroll",
"description": "Responsive mode for mobile devices"
},
"priorityColumns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Columns to show on mobile (priority mode)"
}
}
},
"accessibility": {
"type": "object",
"description": "Accessibility configuration",
"properties": {
"ariaLabel": {
"type": "string",
"description": "ARIA label for the table"
},
"caption": {
"type": "string",
"description": "Table caption for screen readers"
},
"announceChanges": {
"type": "boolean",
"default": true,
"description": "Announce changes to screen readers"
},
"keyboardNavigation": {
"type": "boolean",
"default": true,
"description": "Enable keyboard navigation"
}
}
},
"data": {
"type": "object",
"description": "Data source configuration",
"properties": {
"source": {
"type": "string",
"enum": ["static", "api", "graphql", "websocket"],
"default": "static"
},
"url": {
"type": "string",
"description": "API endpoint for data fetching"
},
"method": {
"type": "string",
"enum": ["GET", "POST"],
"default": "GET"
},
"headers": {
"type": "object",
"description": "HTTP headers for API requests"
},
"refresh": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"interval": {
"type": "number",
"description": "Refresh interval in milliseconds"
}
}
}
}
},
"callbacks": {
"type": "object",
"description": "Event callback configuration",
"properties": {
"onRowClick": {
"type": "string",
"description": "Function name for row click handler"
},
"onSelectionChange": {
"type": "string",
"description": "Function name for selection change handler"
},
"onSort": {
"type": "string",
"description": "Function name for sort change handler"
},
"onFilter": {
"type": "string",
"description": "Function name for filter change handler"
},
"onPageChange": {
"type": "string",
"description": "Function name for page change handler"
},
"onEdit": {
"type": "string",
"description": "Function name for edit handler"
},
"onExport": {
"type": "string",
"description": "Function name for export handler"
}
}
}
},
"required": ["columns"]
}import React, { useState, useRef, useMemo } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import 'ag-grid-enterprise';
/**
* AG Grid Enterprise Example
*
* Demonstrates advanced enterprise features:
* - Row grouping and aggregation
* - Server-side data model
* - Excel export
* - Master-detail (expandable rows)
* - Context menu
* - Column pinning
*
* Requires: npm install ag-grid-react ag-grid-enterprise
* License: Enterprise features require license key
*/
interface SalesData {
id: number;
country: string;
product: string;
sales: number;
profit: number;
quantity: number;
}
const generateData = (): SalesData[] => {
const countries = ['USA', 'UK', 'Germany', 'France', 'Japan'];
const products = ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headset'];
return Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
country: countries[Math.floor(Math.random() * countries.length)],
product: products[Math.floor(Math.random() * products.length)],
sales: Math.floor(Math.random() * 10000),
profit: Math.floor(Math.random() * 3000),
quantity: Math.floor(Math.random() * 100),
}));
};
export function AGGridEnterpriseExample() {
const gridRef = useRef<AgGridReact>(null);
const [rowData] = useState(generateData());
const columnDefs = useMemo(() => [
{
field: 'country',
rowGroup: true, // Group by country
hide: true, // Hide when grouped
},
{
field: 'product',
rowGroup: true, // Then group by product
hide: true,
},
{
field: 'sales',
aggFunc: 'sum', // Sum sales in groups
valueFormatter: (params) => '$' + params.value?.toLocaleString(),
},
{
field: 'profit',
aggFunc: 'sum',
valueFormatter: (params) => '$' + params.value?.toLocaleString(),
},
{
field: 'quantity',
aggFunc: 'avg',
valueFormatter: (params) => params.value?.toFixed(0),
},
], []);
const defaultColDef = useMemo(() => ({
sortable: true,
filter: true,
resizable: true,
enableRowGroup: true,
enablePivot: true,
enableValue: true,
}), []);
const autoGroupColumnDef = useMemo(() => ({
minWidth: 250,
cellRendererParams: {
suppressCount: false, // Show count in group headers
},
}), []);
const exportToExcel = () => {
gridRef.current?.api.exportDataAsExcel({
fileName: 'sales-report.xlsx',
});
};
return (
<div className="p-4">
<div className="mb-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">AG Grid Enterprise Features</h1>
<div className="flex gap-2">
<button
onClick={exportToExcel}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
📥 Export Excel
</button>
<button
onClick={() => gridRef.current?.api.expandAll()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Expand All
</button>
<button
onClick={() => gridRef.current?.api.collapseAll()}
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
>
Collapse All
</button>
</div>
</div>
<div className="ag-theme-alpine" style={{ height: 600, width: '100%' }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
groupDisplayType="multipleColumns"
animateRows={true}
enableRangeSelection={true}
enableCharts={true}
sideBar={true}
statusBar={{
statusPanels: [
{ statusPanel: 'agTotalAndFilteredRowCountComponent' },
{ statusPanel: 'agAggregationComponent' },
],
}}
getContextMenuItems={(params) => [
'copy',
'copyWithHeaders',
'paste',
'separator',
'export',
]}
/>
</div>
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-900">
<strong>Enterprise Features Shown:</strong> Row grouping, aggregation, Excel export,
range selection, charts, sidebar, context menu, status bar
</p>
</div>
</div>
);
}
export default AGGridEnterpriseExample;
import React, { useState, useMemo } from 'react';
/**
* Basic Sortable Table Example
* Demonstrates client-side sorting for small datasets (<100 rows)
*/
interface Person {
id: number;
name: string;
email: string;
department: string;
salary: number;
startDate: string;
}
interface SortConfig {
key: keyof Person | null;
direction: 'ascending' | 'descending';
}
const BasicSortableTable: React.FC = () => {
// Sample data
const data: Person[] = [
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering', salary: 95000, startDate: '2021-03-15' },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Sales', salary: 75000, startDate: '2020-07-22' },
{ id: 3, name: 'Charlie Brown', email: 'charlie@example.com', department: 'Marketing', salary: 82000, startDate: '2022-01-10' },
{ id: 4, name: 'Diana Prince', email: 'diana@example.com', department: 'Engineering', salary: 105000, startDate: '2019-11-05' },
{ id: 5, name: 'Edward Norton', email: 'edward@example.com', department: 'HR', salary: 68000, startDate: '2021-09-20' },
];
const [sortConfig, setSortConfig] = useState<SortConfig>({
key: null,
direction: 'ascending'
});
// Handle sort click
const handleSort = (key: keyof Person) => {
let direction: 'ascending' | 'descending' = 'ascending';
if (sortConfig.key === key) {
if (sortConfig.direction === 'ascending') {
direction = 'descending';
} else {
// Reset sort on third click
setSortConfig({ key: null, direction: 'ascending' });
return;
}
}
setSortConfig({ key, direction });
};
// Sort data
const sortedData = useMemo(() => {
if (!sortConfig.key) return data;
return [...data].sort((a, b) => {
const aValue = a[sortConfig.key as keyof Person];
const bValue = b[sortConfig.key as keyof Person];
if (aValue === null || aValue === undefined) return 1;
if (bValue === null || bValue === undefined) return -1;
if (aValue === bValue) return 0;
// Handle different data types
if (typeof aValue === 'string' && typeof bValue === 'string') {
return sortConfig.direction === 'ascending'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}
// Numeric and date comparison
const comparison = aValue > bValue ? 1 : -1;
return sortConfig.direction === 'ascending' ? comparison : -comparison;
});
}, [data, sortConfig]);
// Get sort indicator
const getSortIndicator = (column: keyof Person) => {
if (sortConfig.key !== column) return <span className="sort-indicator">↕️</span>;
return sortConfig.direction === 'ascending'
? <span className="sort-indicator">↑</span>
: <span className="sort-indicator">↓</span>;
};
// Format currency
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
// Format date
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
return (
<div className="table-container">
<h2>Employee Directory</h2>
<table className="sortable-table" role="table">
<caption>
Click column headers to sort. Click again to reverse order.
</caption>
<thead>
<tr>
<th
scope="col"
onClick={() => handleSort('name')}
className="sortable"
aria-sort={
sortConfig.key === 'name'
? sortConfig.direction === 'ascending'
? 'ascending'
: 'descending'
: 'none'
}
>
Name {getSortIndicator('name')}
</th>
<th
scope="col"
onClick={() => handleSort('email')}
className="sortable"
aria-sort={
sortConfig.key === 'email'
? sortConfig.direction === 'ascending'
? 'ascending'
: 'descending'
: 'none'
}
>
Email {getSortIndicator('email')}
</th>
<th
scope="col"
onClick={() => handleSort('department')}
className="sortable"
aria-sort={
sortConfig.key === 'department'
? sortConfig.direction === 'ascending'
? 'ascending'
: 'descending'
: 'none'
}
>
Department {getSortIndicator('department')}
</th>
<th
scope="col"
onClick={() => handleSort('salary')}
className="sortable align-right"
aria-sort={
sortConfig.key === 'salary'
? sortConfig.direction === 'ascending'
? 'ascending'
: 'descending'
: 'none'
}
>
Salary {getSortIndicator('salary')}
</th>
<th
scope="col"
onClick={() => handleSort('startDate')}
className="sortable"
aria-sort={
sortConfig.key === 'startDate'
? sortConfig.direction === 'ascending'
? 'ascending'
: 'descending'
: 'none'
}
>
Start Date {getSortIndicator('startDate')}
</th>
</tr>
</thead>
<tbody>
{sortedData.map((person) => (
<tr key={person.id}>
<td>{person.name}</td>
<td>
<a href={`mailto:${person.email}`}>{person.email}</a>
</td>
<td>{person.department}</td>
<td className="align-right">{formatCurrency(person.salary)}</td>
<td>{formatDate(person.startDate)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={5} className="summary">
Showing {sortedData.length} employees
{sortConfig.key && (
<span>
{' '}• Sorted by {sortConfig.key} ({sortConfig.direction})
</span>
)}
</td>
</tr>
</tfoot>
</table>
<style jsx>{`
.table-container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
h2 {
margin-bottom: 1rem;
color: var(--color-text-primary, #1a1a1a);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
caption {
padding: 0.75rem;
text-align: left;
font-style: italic;
color: var(--color-text-secondary, #666);
font-size: 0.875rem;
}
th {
background: var(--table-header-bg, #f8f9fa);
padding: 0.75rem 1rem;
text-align: left;
font-weight: 600;
color: var(--table-header-text, #495057);
border-bottom: 2px solid var(--table-border, #dee2e6);
position: relative;
user-select: none;
}
th.sortable {
cursor: pointer;
transition: background-color 0.2s;
}
th.sortable:hover {
background: var(--table-header-hover, #e9ecef);
}
.sort-indicator {
margin-left: 0.5rem;
display: inline-block;
opacity: 0.7;
font-size: 0.875em;
}
td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--table-border, #dee2e6);
}
tbody tr {
transition: background-color 0.15s;
}
tbody tr:hover {
background: var(--table-row-hover, #f8f9fa);
}
tbody tr:nth-child(even) {
background: var(--table-row-even, #fafbfc);
}
.align-right {
text-align: right;
}
a {
color: var(--color-link, #0066cc);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
tfoot td {
padding: 0.75rem 1rem;
background: var(--table-footer-bg, #f8f9fa);
font-size: 0.875rem;
color: var(--color-text-secondary, #666);
}
.summary {
font-weight: 500;
}
/* Responsive */
@media (max-width: 768px) {
table {
font-size: 0.875rem;
}
th, td {
padding: 0.5rem;
}
.sort-indicator {
display: block;
font-size: 0.75em;
}
}
/* Accessibility - Focus styles */
th.sortable:focus {
outline: 2px solid var(--color-focus, #0066cc);
outline-offset: -2px;
}
/* Print styles */
@media print {
.table-container {
max-width: 100%;
}
table {
box-shadow: none;
border: 1px solid #ddd;
}
}
`}</style>
</div>
);
};
export default BasicSortableTable;import React, { useState } from 'react';
import { Check, X } from 'lucide-react';
/**
* Editable Cells Example
*
* Demonstrates inline cell editing with:
* - Double-click to edit
* - Different editor types (text, select, number)
* - Validation
* - Save/cancel actions
* - Optimistic updates
*/
interface Product {
id: number;
name: string;
category: string;
price: number;
stock: number;
}
const initialData: Product[] = [
{ id: 1, name: 'Laptop', category: 'Electronics', price: 1299, stock: 15 },
{ id: 2, name: 'Mouse', category: 'Accessories', price: 29, stock: 50 },
{ id: 3, name: 'Keyboard', category: 'Accessories', price: 79, stock: 30 },
];
export function EditableCellsTable() {
const [data, setData] = useState(initialData);
const [editing, setEditing] = useState<{ rowId: number; field: keyof Product } | null>(null);
const [editValue, setEditValue] = useState<string | number>('');
const startEdit = (rowId: number, field: keyof Product, currentValue: any) => {
setEditing({ rowId, field });
setEditValue(currentValue);
};
const saveEdit = () => {
if (!editing) return;
setData((prev) =>
prev.map((row) =>
row.id === editing.rowId
? { ...row, [editing.field]: editValue }
: row
)
);
setEditing(null);
console.log('Saved:', editing.field, editValue);
};
const cancelEdit = () => {
setEditing(null);
setEditValue('');
};
const renderCell = (row: Product, field: keyof Product) => {
const isEditing = editing?.rowId === row.id && editing?.field === field;
if (isEditing) {
// Editing mode
if (field === 'category') {
return (
<div className="flex items-center gap-2">
<select
value={editValue as string}
onChange={(e) => setEditValue(e.target.value)}
autoFocus
className="px-2 py-1 border rounded"
>
<option value="Electronics">Electronics</option>
<option value="Accessories">Accessories</option>
<option value="Furniture">Furniture</option>
</select>
<button onClick={saveEdit} className="text-green-600">
<Check size={18} />
</button>
<button onClick={cancelEdit} className="text-red-600">
<X size={18} />
</button>
</div>
);
} else {
return (
<div className="flex items-center gap-2">
<input
type={field === 'price' || field === 'stock' ? 'number' : 'text'}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit();
if (e.key === 'Escape') cancelEdit();
}}
autoFocus
className="px-2 py-1 border rounded w-full"
/>
<button onClick={saveEdit} className="text-green-600">
<Check size={18} />
</button>
<button onClick={cancelEdit} className="text-red-600">
<X size={18} />
</button>
</div>
);
}
}
// Display mode
return (
<div
onDoubleClick={() => startEdit(row.id, field, row[field])}
className="cursor-pointer hover:bg-gray-50 px-2 py-1 rounded"
title="Double-click to edit"
>
{field === 'price' ? `$${row[field]}` : row[field]}
</div>
);
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Editable Table</h1>
<div className="mb-4 p-3 bg-blue-50 rounded-lg text-sm text-blue-900">
💡 <strong>Tip:</strong> Double-click any cell to edit. Press Enter to save, Escape to cancel.
</div>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Product Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Category</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Price</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Stock</th>
</tr>
</thead>
<tbody className="bg-white divide-y">
{data.map((row) => (
<tr key={row.id}>
<td className="px-6 py-4 text-gray-500">{row.id}</td>
<td className="px-6 py-4">{renderCell(row, 'name')}</td>
<td className="px-6 py-4">{renderCell(row, 'category')}</td>
<td className="px-6 py-4">{renderCell(row, 'price')}</td>
<td className="px-6 py-4">{renderCell(row, 'stock')}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default EditableCellsTable;
import React, { useState, useMemo } from 'react';
/**
* Paginated Table Example
* Demonstrates client-side pagination for medium datasets (100-1000 rows)
*/
interface DataRow {
id: number;
name: string;
email: string;
role: string;
department: string;
location: string;
status: 'Active' | 'Inactive' | 'Pending';
lastLogin: string;
}
interface PaginationProps {
currentPage: number;
totalPages: number;
pageSize: number;
totalItems: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
}
const PaginationControls: React.FC<PaginationProps> = ({
currentPage,
totalPages,
pageSize,
totalItems,
onPageChange,
onPageSizeChange
}) => {
// Calculate page numbers to display
const getPageNumbers = () => {
const delta = 2;
const range: (number | string)[] = [];
for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) {
range.push(i);
}
if (currentPage - delta > 2) {
range.unshift('...');
}
range.unshift(1);
if (currentPage + delta < totalPages - 1) {
range.push('...');
}
if (totalPages > 1) {
range.push(totalPages);
}
return range;
};
const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, totalItems);
return (
<div className="pagination-controls">
<div className="pagination-info">
<span>
Showing {startItem} to {endItem} of {totalItems} entries
</span>
<select
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
aria-label="Items per page"
>
<option value={10}>10 per page</option>
<option value={25}>25 per page</option>
<option value={50}>50 per page</option>
<option value={100}>100 per page</option>
</select>
</div>
<nav aria-label="Pagination Navigation">
<ul className="pagination">
<li className={currentPage === 1 ? 'disabled' : ''}>
<button
onClick={() => onPageChange(1)}
disabled={currentPage === 1}
aria-label="Go to first page"
>
First
</button>
</li>
<li className={currentPage === 1 ? 'disabled' : ''}>
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Go to previous page"
>
Previous
</button>
</li>
{getPageNumbers().map((number, index) => (
<li
key={index}
className={number === currentPage ? 'active' : number === '...' ? 'ellipsis' : ''}
>
{number === '...' ? (
<span>...</span>
) : (
<button
onClick={() => onPageChange(number as number)}
aria-label={`Go to page ${number}`}
aria-current={number === currentPage ? 'page' : undefined}
>
{number}
</button>
)}
</li>
))}
<li className={currentPage === totalPages ? 'disabled' : ''}>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="Go to next page"
>
Next
</button>
</li>
<li className={currentPage === totalPages ? 'disabled' : ''}>
<button
onClick={() => onPageChange(totalPages)}
disabled={currentPage === totalPages}
aria-label="Go to last page"
>
Last
</button>
</li>
</ul>
</nav>
</div>
);
};
const PaginatedTable: React.FC = () => {
// Generate sample data (simulating larger dataset)
const generateData = (): DataRow[] => {
const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance', 'Operations'];
const roles = ['Manager', 'Senior', 'Junior', 'Lead', 'Analyst', 'Specialist'];
const locations = ['New York', 'San Francisco', 'London', 'Tokyo', 'Berlin', 'Sydney'];
const statuses: ('Active' | 'Inactive' | 'Pending')[] = ['Active', 'Inactive', 'Pending'];
return Array.from({ length: 250 }, (_, i) => ({
id: i + 1,
name: `Employee ${i + 1}`,
email: `employee${i + 1}@example.com`,
role: roles[Math.floor(Math.random() * roles.length)],
department: departments[Math.floor(Math.random() * departments.length)],
location: locations[Math.floor(Math.random() * locations.length)],
status: statuses[Math.floor(Math.random() * statuses.length)],
lastLogin: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString()
}));
};
const allData = useMemo(() => generateData(), []);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
const [searchTerm, setSearchTerm] = useState('');
// Filter data based on search
const filteredData = useMemo(() => {
if (!searchTerm) return allData;
return allData.filter(row =>
Object.values(row).some(value =>
String(value).toLowerCase().includes(searchTerm.toLowerCase())
)
);
}, [allData, searchTerm]);
// Calculate pagination
const totalPages = Math.ceil(filteredData.length / pageSize);
// Get current page data
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return filteredData.slice(startIndex, endIndex);
}, [filteredData, currentPage, pageSize]);
// Handle page size change
const handlePageSizeChange = (newSize: number) => {
setPageSize(newSize);
setCurrentPage(1); // Reset to first page
};
// Handle search
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
setCurrentPage(1); // Reset to first page on search
};
// Format date
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Get status badge class
const getStatusClass = (status: string) => {
switch (status) {
case 'Active': return 'badge-success';
case 'Inactive': return 'badge-danger';
case 'Pending': return 'badge-warning';
default: return 'badge-default';
}
};
return (
<div className="table-container">
<div className="table-header">
<h2>User Management</h2>
<div className="search-box">
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearch}
aria-label="Search table"
/>
{searchTerm && (
<button
onClick={() => {
setSearchTerm('');
setCurrentPage(1);
}}
className="clear-search"
aria-label="Clear search"
>
×
</button>
)}
</div>
</div>
<table role="table">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Department</th>
<th scope="col">Location</th>
<th scope="col">Status</th>
<th scope="col">Last Login</th>
</tr>
</thead>
<tbody>
{paginatedData.map((row) => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.name}</td>
<td>
<a href={`mailto:${row.email}`}>{row.email}</a>
</td>
<td>{row.role}</td>
<td>{row.department}</td>
<td>{row.location}</td>
<td>
<span className={`badge ${getStatusClass(row.status)}`}>
{row.status}
</span>
</td>
<td>{formatDate(row.lastLogin)}</td>
</tr>
))}
{paginatedData.length === 0 && (
<tr>
<td colSpan={8} className="no-data">
{searchTerm ? 'No results found' : 'No data available'}
</td>
</tr>
)}
</tbody>
</table>
<PaginationControls
currentPage={currentPage}
totalPages={totalPages}
pageSize={pageSize}
totalItems={filteredData.length}
onPageChange={setCurrentPage}
onPageSizeChange={handlePageSizeChange}
/>
<style jsx>{`
.table-container {
max-width: 1400px;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
h2 {
margin: 0;
color: var(--color-text-primary, #1a1a1a);
}
.search-box {
position: relative;
width: 300px;
}
.search-box input {
width: 100%;
padding: 0.5rem 2rem 0.5rem 1rem;
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
font-size: 0.875rem;
}
.clear-search {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0 0.25rem;
color: var(--color-text-secondary, #666);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
th {
background: var(--table-header-bg, #f8f9fa);
padding: 0.75rem 1rem;
text-align: left;
font-weight: 600;
color: var(--table-header-text, #495057);
border-bottom: 2px solid var(--table-border, #dee2e6);
}
td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--table-border, #dee2e6);
}
tbody tr:hover {
background: var(--table-row-hover, #f8f9fa);
}
.no-data {
text-align: center;
color: var(--color-text-secondary, #666);
padding: 2rem;
}
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 4px;
text-transform: uppercase;
}
.badge-success {
background: #d4edda;
color: #155724;
}
.badge-danger {
background: #f8d7da;
color: #721c24;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
/* Pagination styles */
.pagination-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
padding: 1rem;
background: var(--table-footer-bg, #f8f9fa);
border-radius: 0 0 8px 8px;
}
.pagination-info {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.875rem;
color: var(--color-text-secondary, #666);
}
.pagination-info select {
padding: 0.25rem 0.5rem;
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
}
.pagination {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 0.25rem;
}
.pagination li button {
padding: 0.5rem 0.75rem;
background: white;
border: 1px solid var(--border-color, #ddd);
color: var(--color-text-primary, #1a1a1a);
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}
.pagination li button:hover:not(:disabled) {
background: var(--color-primary-light, #e3f2fd);
border-color: var(--color-primary, #2196f3);
}
.pagination li.active button {
background: var(--color-primary, #2196f3);
color: white;
border-color: var(--color-primary, #2196f3);
}
.pagination li.disabled button {
opacity: 0.5;
cursor: not-allowed;
}
.pagination li.ellipsis span {
padding: 0.5rem 0.75rem;
color: var(--color-text-secondary, #666);
}
/* Responsive */
@media (max-width: 768px) {
.table-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.search-box {
width: 100%;
}
.pagination-controls {
flex-direction: column;
gap: 1rem;
}
table {
font-size: 0.875rem;
}
th, td {
padding: 0.5rem;
}
}
`}</style>
</div>
);
};
export default PaginatedTable;import React from 'react';
/**
* Responsive Table Patterns Comparison
*
* Demonstrates 3 different responsive strategies:
* 1. Horizontal scroll
* 2. Priority columns (hide less important on mobile)
* 3. Card layout transformation
*/
interface Order {
id: number;
customer: string;
product: string;
amount: number;
status: string;
date: string;
}
const orders: Order[] = [
{ id: 1001, customer: 'John Doe', product: 'Laptop Pro', amount: 1299, status: 'Shipped', date: '2025-12-01' },
{ id: 1002, customer: 'Jane Smith', product: 'Mouse', amount: 29, status: 'Processing', date: '2025-12-02' },
{ id: 1003, customer: 'Bob Johnson', product: 'Monitor', amount: 399, status: 'Delivered', date: '2025-12-03' },
];
// Pattern 1: Horizontal Scroll
function HorizontalScrollTable() {
return (
<div>
<h2 className="text-lg font-semibold mb-2">Pattern 1: Horizontal Scroll</h2>
<div className="overflow-x-auto border rounded-lg">
<table className="min-w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left">Order ID</th>
<th className="px-6 py-3 text-left">Customer</th>
<th className="px-6 py-3 text-left">Product</th>
<th className="px-6 py-3 text-left">Amount</th>
<th className="px-6 py-3 text-left">Status</th>
<th className="px-6 py-3 text-left">Date</th>
</tr>
</thead>
<tbody className="divide-y">
{orders.map((order) => (
<tr key={order.id}>
<td className="px-6 py-4">{order.id}</td>
<td className="px-6 py-4">{order.customer}</td>
<td className="px-6 py-4">{order.product}</td>
<td className="px-6 py-4">${order.amount}</td>
<td className="px-6 py-4">{order.status}</td>
<td className="px-6 py-4">{order.date}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-gray-600 mt-2">✓ Simple | ✗ Poor mobile UX (horizontal scroll)</p>
</div>
);
}
// Pattern 2: Priority Columns
function PriorityColumnsTable() {
return (
<div>
<h2 className="text-lg font-semibold mb-2">Pattern 2: Priority Columns</h2>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left">Order ID</th>
<th className="px-6 py-3 text-left">Customer</th>
<th className="px-6 py-3 text-left hidden md:table-cell">Product</th>
<th className="px-6 py-3 text-left">Amount</th>
<th className="px-6 py-3 text-left hidden lg:table-cell">Status</th>
<th className="px-6 py-3 text-left hidden lg:table-cell">Date</th>
</tr>
</thead>
<tbody className="divide-y">
{orders.map((order) => (
<tr key={order.id}>
<td className="px-6 py-4">{order.id}</td>
<td className="px-6 py-4">{order.customer}</td>
<td className="px-6 py-4 hidden md:table-cell">{order.product}</td>
<td className="px-6 py-4 font-semibold">${order.amount}</td>
<td className="px-6 py-4 hidden lg:table-cell">{order.status}</td>
<td className="px-6 py-4 hidden lg:table-cell">{order.date}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-gray-600 mt-2">
✓ Moderate complexity | ✓ Shows most important columns | ✗ Hidden data not accessible
</p>
</div>
);
}
// Pattern 3: Card Layout
function CardLayoutTable() {
return (
<div>
<h2 className="text-lg font-semibold mb-2">Pattern 3: Card Layout (Mobile-Optimized)</h2>
<div className="space-y-4 md:hidden">
{orders.map((order) => (
<div key={order.id} className="border rounded-lg p-4 bg-white">
<div className="flex justify-between items-start mb-2">
<span className="text-xs text-gray-500">Order #{order.id}</span>
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">
{order.status}
</span>
</div>
<h3 className="font-semibold text-lg mb-1">{order.customer}</h3>
<p className="text-sm text-gray-600 mb-2">{order.product}</p>
<div className="flex justify-between items-center pt-2 border-t">
<span className="font-bold text-lg text-blue-600">${order.amount}</span>
<span className="text-sm text-gray-500">{order.date}</span>
</div>
</div>
))}
</div>
<div className="hidden md:block overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left">Order ID</th>
<th className="px-6 py-3 text-left">Customer</th>
<th className="px-6 py-3 text-left">Product</th>
<th className="px-6 py-3 text-left">Amount</th>
<th className="px-6 py-3 text-left">Status</th>
<th className="px-6 py-3 text-left">Date</th>
</tr>
</thead>
<tbody className="divide-y">
{orders.map((order) => (
<tr key={order.id}>
<td className="px-6 py-4">{order.id}</td>
<td className="px-6 py-4">{order.customer}</td>
<td className="px-6 py-4">{order.product}</td>
<td className="px-6 py-4">${order.amount}</td>
<td className="px-6 py-4">{order.status}</td>
<td className="px-6 py-4">{order.date}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-gray-600 mt-2">
✓ Best mobile UX | ✓ All data accessible | ✗ More code complexity
</p>
</div>
);
}
export function ResponsivePatternsComparison() {
return (
<div className="p-4 space-y-8">
<div>
<h1 className="text-2xl font-bold mb-2">Responsive Table Patterns</h1>
<p className="text-gray-600 mb-6">
Resize your browser to see how each pattern adapts to mobile screens.
</p>
</div>
<HorizontalScrollTable />
<PriorityColumnsTable />
<CardLayoutTable />
<div className="mt-8 p-4 bg-gray-100 rounded-lg">
<h3 className="font-semibold mb-2">Recommendation:</h3>
<ul className="text-sm space-y-1 text-gray-700">
<li>• <strong>Quick implementation:</strong> Horizontal scroll</li>
<li>• <strong>Moderate columns (5-8):</strong> Priority columns</li>
<li>• <strong>Many columns (8+) or mobile-first:</strong> Card layout</li>
<li>• <strong>Enterprise apps:</strong> AG Grid responsive features</li>
</ul>
</div>
</div>
);
}
export default ResponsivePatternsComparison;
import React, { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';
/**
* Server-Side Sorting and Filtering Example
*
* For large datasets (100K+ rows), perform sorting/filtering on the server.
* Frontend sends sort/filter parameters, backend returns paginated results.
*
* API format: GET /api/users?page=1&pageSize=20&sortBy=name&sortOrder=asc&filter={"role":"admin"}
*/
interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: string;
}
interface TableState {
page: number;
pageSize: number;
sortBy: string | null;
sortOrder: 'asc' | 'desc';
filters: Record<string, string>;
}
async function fetchUsers(state: TableState) {
const params = new URLSearchParams({
page: state.page.toString(),
pageSize: state.pageSize.toString(),
...(state.sortBy && { sortBy: state.sortBy, sortOrder: state.sortOrder }),
...(Object.keys(state.filters).length && { filter: JSON.stringify(state.filters) }),
});
const response = await fetch(`/api/users?${params}`);
return response.json();
}
export function ServerSideSortingTable() {
const [tableState, setTableState] = useState<TableState>({
page: 1,
pageSize: 20,
sortBy: null,
sortOrder: 'asc',
filters: {},
});
const { data, isLoading } = useQuery({
queryKey: ['users', tableState],
queryFn: () => fetchUsers(tableState),
keepPreviousData: true, // Show old data while fetching new
});
const handleSort = (column: string) => {
setTableState((prev) => ({
...prev,
sortBy: column,
sortOrder: prev.sortBy === column && prev.sortOrder === 'asc' ? 'desc' : 'asc',
page: 1, // Reset to first page on sort
}));
};
const handleFilter = (column: string, value: string) => {
setTableState((prev) => ({
...prev,
filters: value ? { ...prev.filters, [column]: value } : { ...prev.filters, [column]: undefined },
page: 1, // Reset to first page on filter
}));
};
const SortIcon = ({ column }: { column: string }) => {
if (tableState.sortBy !== column) {
return <ChevronsUpDown size={14} className="text-gray-400" />;
}
return tableState.sortOrder === 'asc' ? (
<ChevronUp size={14} className="text-blue-600" />
) : (
<ChevronDown size={14} className="text-blue-600" />
);
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Server-Side Table (100K+ rows)</h1>
<div className="mb-4 p-3 bg-yellow-50 rounded-lg text-sm text-yellow-900">
ℹ️ Sorting and filtering performed on server. Efficient for large datasets.
</div>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
{['name', 'email', 'role', 'createdAt'].map((col) => (
<th key={col} className="px-6 py-3 text-left">
<div
onClick={() => handleSort(col)}
className="flex items-center gap-2 cursor-pointer hover:bg-gray-100 -mx-2 px-2 py-1 rounded"
>
<span className="text-xs font-medium text-gray-500 uppercase">
{col}
</span>
<SortIcon column={col} />
</div>
{/* Column filter */}
<input
type="text"
placeholder={`Filter ${col}...`}
value={tableState.filters[col] || ''}
onChange={(e) => handleFilter(col, e.target.value)}
onClick={(e) => e.stopPropagation()}
className="mt-2 px-2 py-1 text-xs border rounded w-full"
/>
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y">
{isLoading ? (
// Loading skeleton
Array.from({ length: 5 }).map((_, i) => (
<tr key={i}>
{Array.from({ length: 4 }).map((_, j) => (
<td key={j} className="px-6 py-4">
<div className="h-4 bg-gray-200 rounded animate-pulse" />
</td>
))}
</tr>
))
) : (
data?.users.map((user: User) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 font-medium">{user.name}</td>
<td className="px-6 py-4 text-gray-600">{user.email}</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
{user.role}
</span>
</td>
<td className="px-6 py-4 text-gray-600">{user.createdAt}</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-700">
Showing page {tableState.page} of {data?.totalPages || 1} ({data?.totalCount || 0} total)
</div>
<div className="flex gap-2">
<button
onClick={() => setTableState({ ...tableState, page: Math.max(1, tableState.page - 1) })}
disabled={tableState.page === 1}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => setTableState({ ...tableState, page: tableState.page + 1 })}
disabled={tableState.page >= (data?.totalPages || 1)}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Next
</button>
</div>
</div>
</div>
);
}
export default ServerSideSortingTable;
import React from 'react';
/**
* Simple Responsive Table Example
*
* Transforms from table layout (desktop) to card layout (mobile)
* using CSS media queries and Tailwind responsive utilities.
*/
interface User {
id: number;
name: string;
email: string;
role: string;
joinDate: string;
}
const users: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', joinDate: '2025-01-15' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'Editor', joinDate: '2025-03-22' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'Viewer', joinDate: '2025-06-10' },
];
export function SimpleResponsiveTable() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Users</h1>
{/* Desktop: Table */}
<div className="hidden md:block overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Role</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Join Date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y">
{users.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap font-medium">{user.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-gray-600">{user.email}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
{user.role}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-gray-600">{user.joinDate}</td>
<td className="px-6 py-4 whitespace-nowrap">
<button className="text-blue-600 hover:text-blue-800 mr-2">Edit</button>
<button className="text-red-600 hover:text-red-800">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: Cards */}
<div className="md:hidden space-y-4">
{users.map((user) => (
<div key={user.id} className="border rounded-lg p-4 bg-white shadow-sm">
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="font-semibold text-lg">{user.name}</h3>
<p className="text-sm text-gray-600">{user.email}</p>
</div>
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
{user.role}
</span>
</div>
<div className="text-sm text-gray-600 mb-3">
Joined: {user.joinDate}
</div>
<div className="flex gap-2">
<button className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg">
Edit
</button>
<button className="flex-1 px-4 py-2 bg-white border border-red-500 text-red-500 rounded-lg">
Delete
</button>
</div>
</div>
))}
</div>
{/* Summary */}
<div className="mt-4 text-sm text-gray-600">
Total: {users.length} users
</div>
</div>
);
}
export default SimpleResponsiveTable;
import React, { useState, useMemo } from 'react';
import { ChevronUp, ChevronDown, Filter } from 'lucide-react';
/**
* Sortable and Filterable Table Example
*
* Features:
* - Multi-column sorting
* - Per-column filters
* - Global search
* - Filter chips
* - Clear all filters
*/
interface Product {
id: number;
name: string;
category: string;
price: number;
stock: number;
status: 'In Stock' | 'Low Stock' | 'Out of Stock';
}
const products: Product[] = [
{ id: 1, name: 'Laptop Pro', category: 'Electronics', price: 1299, stock: 15, status: 'In Stock' },
{ id: 2, name: 'Wireless Mouse', category: 'Accessories', price: 29, stock: 3, status: 'Low Stock' },
{ id: 3, name: 'Keyboard', category: 'Accessories', price: 79, stock: 0, status: 'Out of Stock' },
{ id: 4, name: 'Monitor 27"', category: 'Electronics', price: 399, stock: 8, status: 'In Stock' },
{ id: 5, name: 'USB-C Hub', category: 'Accessories', price: 49, stock: 25, status: 'In Stock' },
];
export function SortableFilteredTable() {
const [sortColumn, setSortColumn] = useState<keyof Product | null>(null);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [filters, setFilters] = useState({
category: '',
status: '',
search: '',
});
const handleSort = (column: keyof Product) => {
if (sortColumn === column) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortColumn(column);
setSortDirection('asc');
}
};
const filteredAndSortedData = useMemo(() => {
let result = [...products];
// Apply filters
if (filters.category) {
result = result.filter((p) => p.category === filters.category);
}
if (filters.status) {
result = result.filter((p) => p.status === filters.status);
}
if (filters.search) {
result = result.filter((p) =>
p.name.toLowerCase().includes(filters.search.toLowerCase())
);
}
// Apply sorting
if (sortColumn) {
result.sort((a, b) => {
const aVal = a[sortColumn];
const bVal = b[sortColumn];
if (sortDirection === 'asc') {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
}
return result;
}, [filters, sortColumn, sortDirection]);
const clearFilters = () => {
setFilters({ category: '', status: '', search: '' });
};
const activeFilterCount = Object.values(filters).filter(Boolean).length;
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Product Inventory</h1>
{/* Filters */}
<div className="mb-4 flex gap-4 flex-wrap">
<input
type="text"
placeholder="Search products..."
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
className="px-4 py-2 border rounded-lg w-64"
/>
<select
value={filters.category}
onChange={(e) => setFilters({ ...filters, category: e.target.value })}
className="px-4 py-2 border rounded-lg"
>
<option value="">All Categories</option>
<option value="Electronics">Electronics</option>
<option value="Accessories">Accessories</option>
</select>
<select
value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
className="px-4 py-2 border rounded-lg"
>
<option value="">All Status</option>
<option value="In Stock">In Stock</option>
<option value="Low Stock">Low Stock</option>
<option value="Out of Stock">Out of Stock</option>
</select>
{activeFilterCount > 0 && (
<button
onClick={clearFilters}
className="px-4 py-2 text-blue-600 hover:text-blue-800"
>
Clear Filters ({activeFilterCount})
</button>
)}
</div>
{/* Results count */}
<div className="mb-2 text-sm text-gray-600">
Showing {filteredAndSortedData.length} of {products.length} products
</div>
{/* Table */}
<div className="overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
{(['name', 'category', 'price', 'stock', 'status'] as const).map((col) => (
<th
key={col}
onClick={() => handleSort(col)}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center gap-2">
{col}
{sortColumn === col && (
sortDirection === 'asc' ? <ChevronUp size={16} /> : <ChevronDown size={16} />
)}
</div>
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y">
{filteredAndSortedData.map((product) => (
<tr key={product.id} className="hover:bg-gray-50">
<td className="px-6 py-4">{product.name}</td>
<td className="px-6 py-4">{product.category}</td>
<td className="px-6 py-4">${product.price}</td>
<td className="px-6 py-4">{product.stock}</td>
<td className="px-6 py-4">
<span
className={`px-2 py-1 rounded text-xs ${
product.status === 'In Stock'
? 'bg-green-100 text-green-800'
: product.status === 'Low Stock'
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}
>
{product.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default SortableFilteredTable;
import React, { useState, useEffect } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
SortingState,
ColumnFiltersState,
VisibilityState,
ColumnDef,
} from '@tanstack/react-table';
/**
* Table State Persistence Example
*
* Saves and restores table state to localStorage:
* - Column sorting
* - Column filters
* - Column visibility
* - Pagination state
* - Column order
* - Column sizing
*/
interface User {
id: number;
name: string;
email: string;
role: string;
department: string;
}
const data: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', department: 'IT' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'Manager', department: 'Sales' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'Developer', department: 'Engineering' },
// Add more data...
];
const columns: ColumnDef<User>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'role', header: 'Role' },
{ accessorKey: 'department', header: 'Department' },
];
const STATE_KEY = 'table-state-v1';
export function TableStatePersistence() {
// Load state from localStorage
const loadState = () => {
const saved = localStorage.getItem(STATE_KEY);
if (saved) {
try {
return JSON.parse(saved);
} catch {
return {};
}
}
return {};
};
const savedState = loadState();
const [sorting, setSorting] = useState<SortingState>(savedState.sorting || []);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(savedState.columnFilters || []);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(savedState.columnVisibility || {});
const [pagination, setPagination] = useState(savedState.pagination || { pageIndex: 0, pageSize: 10 });
const table = useReactTable({
data,
columns,
state: {
sorting,
columnFilters,
columnVisibility,
pagination,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
// Save state to localStorage whenever it changes
useEffect(() => {
const state = {
sorting,
columnFilters,
columnVisibility,
pagination,
};
localStorage.setItem(STATE_KEY, JSON.stringify(state));
}, [sorting, columnFilters, columnVisibility, pagination]);
const resetState = () => {
localStorage.removeItem(STATE_KEY);
setSorting([]);
setColumnFilters([]);
setColumnVisibility({});
setPagination({ pageIndex: 0, pageSize: 10 });
};
return (
<div className="p-4">
<div className="mb-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">Table with Persistent State</h1>
<button
onClick={resetState}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Reset State
</button>
</div>
<div className="mb-4 p-3 bg-green-50 rounded-lg text-sm text-green-900">
✓ Table state is automatically saved to localStorage and restored on page reload
</div>
{/* Column visibility toggles */}
<div className="mb-4 flex gap-2 flex-wrap">
{table.getAllLeafColumns().map((column) => (
<label key={column.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/>
<span className="text-sm">{column.id}</span>
</label>
))}
</div>
{/* Table */}
<div className="overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} className="px-6 py-3 text-left">
<div
onClick={header.column.getToggleSortingHandler()}
className="cursor-pointer select-none"
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: ' ↑',
desc: ' ↓',
}[header.column.getIsSorted() as string] ?? null}
</div>
{/* Column filter */}
{header.column.getCanFilter() && (
<input
type="text"
value={(header.column.getFilterValue() as string) ?? ''}
onChange={(e) => header.column.setFilterValue(e.target.value)}
placeholder="Filter..."
className="mt-2 px-2 py-1 text-xs border rounded w-full"
onClick={(e) => e.stopPropagation()}
/>
)}
</th>
))}
</tr>
))}
</thead>
<tbody className="bg-white divide-y">
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="mt-4 flex justify-between items-center">
<span className="text-sm text-gray-700">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</span>
<div className="flex gap-2">
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Next
</button>
</div>
</div>
</div>
);
}
export default TableStatePersistence;
import React, { useState } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
SortingState,
ColumnDef,
} from '@tanstack/react-table';
/**
* TanStack Table Basic Example
*
* Demonstrates core TanStack Table features:
* - Column definitions
* - Sorting (click headers)
* - Global filtering (search)
* - Pagination
* - Responsive design
*/
interface User {
id: number;
name: string;
email: string;
role: string;
status: 'active' | 'inactive';
}
const sampleData: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'active' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User', status: 'active' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User', status: 'inactive' },
// ... more data
];
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: (info) => info.getValue(),
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'role',
header: 'Role',
},
{
accessorKey: 'status',
header: 'Status',
cell: (info) => {
const status = info.getValue() as string;
return (
<span
className={`px-2 py-1 rounded text-xs ${
status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}
>
{status}
</span>
);
},
},
];
export function TanStackBasicTable() {
const [data] = useState(sampleData);
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: {
sorting,
globalFilter,
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: {
pagination: { pageSize: 10 },
},
});
return (
<div className="p-4">
<div className="mb-4 flex justify-between">
<input
type="text"
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search..."
className="px-4 py-2 border rounded-lg w-64"
/>
</div>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full">
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
>
<div className="flex items-center gap-2">
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: ' ↑',
desc: ' ↓',
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="mt-4 flex items-center justify-between">
<div className="text-sm text-gray-700">
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{' '}
{Math.min(
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
table.getFilteredRowModel().rows.length
)}{' '}
of {table.getFilteredRowModel().rows.length} results
</div>
<div className="flex gap-2">
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Next
</button>
</div>
</div>
</div>
);
}
export default TanStackBasicTable;
import React, { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
/**
* Virtual Scrolling Table Example
*
* Efficiently renders large datasets (10K+ rows) by only rendering visible rows.
* Uses @tanstack/react-virtual for performance.
*
* Install: npm install @tanstack/react-virtual
*/
interface DataRow {
id: number;
name: string;
value: number;
category: string;
}
// Generate large dataset
const generateData = (count: number): DataRow[] => {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
value: Math.floor(Math.random() * 1000),
category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)],
}));
};
export function VirtualScrollingGrid() {
const parentRef = useRef<HTMLDivElement>(null);
const data = useRef(generateData(10000)).current; // 10,000 rows
const virtualizer = useVirtualizer({
count: data.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Estimated row height
overscan: 20, // Render 20 extra rows above/below viewport
});
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">
Virtual Scrolling Grid ({data.length.toLocaleString()} rows)
</h1>
<div
ref={parentRef}
className="border rounded-lg"
style={{
height: '600px',
overflow: 'auto',
}}
>
{/* Header (sticky) */}
<div
style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#f9fafb',
borderBottom: '1px solid #e5e7eb',
display: 'grid',
gridTemplateColumns: '80px 1fr 120px 120px',
padding: '12px 16px',
fontWeight: 600,
fontSize: '14px',
color: '#6b7280',
}}
>
<div>ID</div>
<div>Name</div>
<div>Value</div>
<div>Category</div>
</div>
{/* Virtual rows */}
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = data[virtualRow.index];
return (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
display: 'grid',
gridTemplateColumns: '80px 1fr 120px 120px',
padding: '12px 16px',
borderBottom: '1px solid #f3f4f6',
backgroundColor: virtualRow.index % 2 === 0 ? '#fff' : '#f9fafb',
}}
>
<div className="text-gray-500">{row.id}</div>
<div className="font-medium">{row.name}</div>
<div className="text-blue-600">${row.value}</div>
<div>
<span className="px-2 py-1 bg-gray-100 rounded text-xs">
{row.category}
</span>
</div>
</div>
);
})}
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
Only rendering ~{virtualizer.getVirtualItems().length} rows at a time (out of {data.length.toLocaleString()})
</div>
</div>
);
}
export default VirtualScrollingGrid;
import React, { useRef, useState, useEffect, useCallback } from 'react';
/**
* Virtual Scrolling Table Example
* Demonstrates high-performance rendering for large datasets (10,000+ rows)
* Only renders visible rows in the viewport for optimal performance
*/
interface DataRow {
id: number;
name: string;
email: string;
company: string;
revenue: number;
employees: number;
founded: number;
industry: string;
status: string;
}
interface VirtualScrollProps {
data: DataRow[];
rowHeight: number;
containerHeight: number;
overscan?: number; // Number of rows to render outside viewport
}
const VirtualScrollTable: React.FC<VirtualScrollProps> = ({
data,
rowHeight,
containerHeight,
overscan = 5
}) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const [isScrolling, setIsScrolling] = useState(false);
// Calculate visible range
const visibleRowCount = Math.ceil(containerHeight / rowHeight);
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const endIndex = Math.min(
data.length,
Math.ceil((scrollTop + containerHeight) / rowHeight) + overscan
);
// Total height for scrollbar
const totalHeight = data.length * rowHeight;
// Get visible items
const visibleItems = data.slice(startIndex, endIndex);
// Handle scroll with debouncing
const scrollTimeout = useRef<NodeJS.Timeout>();
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const newScrollTop = e.currentTarget.scrollTop;
setScrollTop(newScrollTop);
setIsScrolling(true);
// Clear existing timeout
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
// Set scrolling to false after scrolling stops
scrollTimeout.current = setTimeout(() => {
setIsScrolling(false);
}, 150);
}, []);
// Format number
const formatNumber = (num: number) => {
return new Intl.NumberFormat('en-US').format(num);
};
// Format currency
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
notation: 'compact',
maximumFractionDigits: 1
}).format(amount);
};
return (
<div className="virtual-table-wrapper">
<div className="table-header-fixed">
<table>
<thead>
<tr>
<th style={{ width: '80px' }}>ID</th>
<th style={{ width: '200px' }}>Company</th>
<th style={{ width: '250px' }}>Contact</th>
<th style={{ width: '150px' }}>Revenue</th>
<th style={{ width: '120px' }}>Employees</th>
<th style={{ width: '100px' }}>Founded</th>
<th style={{ width: '150px' }}>Industry</th>
<th style={{ width: '100px' }}>Status</th>
</tr>
</thead>
</table>
</div>
<div
ref={scrollContainerRef}
className="scroll-container"
style={{ height: containerHeight }}
onScroll={handleScroll}
>
<div
className="scroll-height"
style={{ height: totalHeight }}
>
<div
className="visible-window"
style={{
transform: `translateY(${startIndex * rowHeight}px)`
}}
>
<table>
<tbody>
{visibleItems.map((row, index) => (
<tr
key={row.id}
style={{ height: rowHeight }}
className={isScrolling ? 'scrolling' : ''}
>
<td style={{ width: '80px' }}>{row.id}</td>
<td style={{ width: '200px' }}>{row.company}</td>
<td style={{ width: '250px' }}>
<div className="contact-cell">
<div>{row.name}</div>
<div className="email">{row.email}</div>
</div>
</td>
<td style={{ width: '150px' }}>{formatCurrency(row.revenue)}</td>
<td style={{ width: '120px' }}>{formatNumber(row.employees)}</td>
<td style={{ width: '100px' }}>{row.founded}</td>
<td style={{ width: '150px' }}>{row.industry}</td>
<td style={{ width: '100px' }}>
<span className={`status status-${row.status.toLowerCase()}`}>
{row.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
<div className="table-footer">
<div className="footer-info">
Rendering rows {startIndex + 1} - {Math.min(endIndex, data.length)} of {formatNumber(data.length)}
</div>
<div className="performance-info">
<span className="metric">
DOM Nodes: <strong>{endIndex - startIndex}</strong>
</span>
<span className="metric">
Memory: <strong>~{((endIndex - startIndex) * 0.5).toFixed(1)}KB</strong>
</span>
<span className="metric">
Status: <strong className={isScrolling ? 'scrolling' : 'idle'}>
{isScrolling ? 'Scrolling' : 'Idle'}
</strong>
</span>
</div>
</div>
<style jsx>{`
.virtual-table-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.table-header-fixed {
position: sticky;
top: 0;
z-index: 10;
background: white;
border-bottom: 2px solid var(--table-border, #dee2e6);
}
.scroll-container {
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
.scroll-height {
position: relative;
width: 100%;
}
.visible-window {
position: absolute;
top: 0;
left: 0;
right: 0;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th {
padding: 1rem;
text-align: left;
font-weight: 600;
background: var(--table-header-bg, #f8f9fa);
color: var(--table-header-text, #495057);
position: sticky;
top: 0;
}
td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--table-border, #e9ecef);
}
tr {
background: white;
transition: background-color 0.15s;
}
tr:hover {
background: var(--table-row-hover, #f8f9fa);
}
tr.scrolling {
pointer-events: none;
}
.contact-cell {
line-height: 1.3;
}
.email {
font-size: 0.85em;
color: var(--color-text-secondary, #6c757d);
}
.status {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
.table-footer {
display: flex;
justify-content: space-between;
padding: 1rem;
background: var(--table-footer-bg, #f8f9fa);
border-top: 1px solid var(--table-border, #dee2e6);
font-size: 0.875rem;
}
.footer-info {
color: var(--color-text-secondary, #6c757d);
}
.performance-info {
display: flex;
gap: 1.5rem;
}
.metric {
color: var(--color-text-secondary, #6c757d);
}
.metric strong {
color: var(--color-text-primary, #212529);
font-weight: 600;
}
.metric .scrolling {
color: var(--color-warning, #ffc107);
}
.metric .idle {
color: var(--color-success, #28a745);
}
/* Custom scrollbar */
.scroll-container::-webkit-scrollbar {
width: 12px;
}
.scroll-container::-webkit-scrollbar-track {
background: #f1f1f1;
}
.scroll-container::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.scroll-container::-webkit-scrollbar-thumb:hover {
background: #555;
}
`}</style>
</div>
);
};
// Main component with data generation
const VirtualScrollingExample: React.FC = () => {
const [rowCount, setRowCount] = useState(10000);
const [data, setData] = useState<DataRow[]>([]);
// Generate large dataset
useEffect(() => {
const industries = [
'Technology', 'Finance', 'Healthcare', 'Retail', 'Manufacturing',
'Energy', 'Telecommunications', 'Transportation', 'Real Estate', 'Education'
];
const statuses = ['Active', 'Pending', 'Inactive'];
const companies = [
'Acme Corp', 'Global Tech', 'Innovate Inc', 'Digital Solutions', 'Future Systems',
'Smart Industries', 'Quantum Dynamics', 'Nexus Enterprises', 'Apex Innovations', 'Synergy Group'
];
const firstNames = ['John', 'Jane', 'Michael', 'Sarah', 'Robert', 'Lisa', 'David', 'Emma'];
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis'];
const newData: DataRow[] = Array.from({ length: rowCount }, (_, i) => ({
id: i + 1,
name: `${firstNames[Math.floor(Math.random() * firstNames.length)]} ${lastNames[Math.floor(Math.random() * lastNames.length)]}`,
email: `contact${i + 1}@company.com`,
company: `${companies[Math.floor(Math.random() * companies.length)]} ${i + 1}`,
revenue: Math.floor(Math.random() * 100000000) + 1000000,
employees: Math.floor(Math.random() * 10000) + 10,
founded: Math.floor(Math.random() * 50) + 1970,
industry: industries[Math.floor(Math.random() * industries.length)],
status: statuses[Math.floor(Math.random() * statuses.length)]
}));
setData(newData);
}, [rowCount]);
return (
<div className="container">
<div className="header">
<h1>Virtual Scrolling Table</h1>
<div className="controls">
<label>
Row Count:
<select
value={rowCount}
onChange={(e) => setRowCount(Number(e.target.value))}
>
<option value={1000}>1,000 rows</option>
<option value={10000}>10,000 rows</option>
<option value={50000}>50,000 rows</option>
<option value={100000}>100,000 rows</option>
<option value={500000}>500,000 rows</option>
<option value={1000000}>1,000,000 rows</option>
</select>
</label>
<div className="info">
Only visible rows are rendered in the DOM for optimal performance
</div>
</div>
</div>
<VirtualScrollTable
data={data}
rowHeight={60}
containerHeight={600}
overscan={5}
/>
<style jsx>{`
.container {
max-width: 1400px;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
.header {
margin-bottom: 2rem;
}
h1 {
margin-bottom: 1rem;
color: var(--color-text-primary, #212529);
}
.controls {
display: flex;
align-items: center;
gap: 2rem;
}
label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
}
select {
padding: 0.5rem;
border: 1px solid var(--border-color, #dee2e6);
border-radius: 4px;
font-size: 1rem;
}
.info {
color: var(--color-text-secondary, #6c757d);
font-style: italic;
}
`}</style>
</div>
);
};
export default VirtualScrollingExample;skill: "building-tables"
version: "1.0"
domain: "frontend"
base_outputs:
- path: "components/Table.tsx"
must_contain: ["table", "thead", "tbody", "tr", "td", "th"]
description: "Core table component with semantic HTML structure"
- path: "types/table.ts"
must_contain: ["interface", "ColumnDef", "TableProps"]
description: "TypeScript type definitions for table data and props"
conditional_outputs:
maturity:
starter:
- path: "components/SimpleTable.tsx"
must_contain: ["table", "map", "thead", "tbody"]
description: "Basic HTML table with no external dependencies (< 100 rows)"
- path: "components/SortableTable.tsx"
must_contain: ["useState", "sort", "onClick"]
description: "Client-side sortable table with click handlers"
intermediate:
- path: "components/DataTable.tsx"
must_contain: ["useReactTable", "getCoreRowModel", "getSortedRowModel"]
description: "Feature-rich table using TanStack Table with sorting/filtering (100-10K rows)"
- path: "components/PaginatedTable.tsx"
must_contain: ["getPaginationRowModel", "pageIndex", "pageSize"]
description: "Table with pagination controls"
- path: "hooks/useTableFilters.ts"
must_contain: ["useState", "useMemo", "filter"]
description: "Custom hook for managing table filtering logic"
- path: "components/TableSearch.tsx"
must_contain: ["input", "onChange", "globalFilter"]
description: "Global search component for tables"
advanced:
- path: "components/VirtualTable.tsx"
must_contain: ["useVirtualizer", "virtualizer", "getVirtualItems"]
description: "Virtual scrolling table for large datasets (10K+ rows)"
- path: "components/EditableTable.tsx"
must_contain: ["onUpdate", "validation", "useState", "editable"]
description: "Table with inline editing and validation"
- path: "components/ServerTable.tsx"
must_contain: ["useQuery", "fetch", "api", "pagination"]
description: "Server-side table with API integration"
- path: "hooks/useTableState.ts"
must_contain: ["localStorage", "useEffect", "persistence"]
description: "Hook for persisting table state (sort, filters, columns)"
- path: "utils/tableExport.ts"
must_contain: ["export", "csv", "download"]
description: "Utility functions for exporting table data (CSV/Excel)"
frontend_framework:
react:
- path: "components/ReactTable.tsx"
must_contain: ["useReactTable", "flexRender", "React"]
description: "React-specific table implementation using TanStack Table"
- path: "hooks/useTableData.ts"
must_contain: ["useState", "useEffect", "useMemo"]
description: "React hooks for managing table data and state"
vue:
- path: "components/VueTable.vue"
must_contain: ["<template>", "<script setup>", "ref", "computed"]
description: "Vue 3 table component with Composition API"
angular:
- path: "components/angular-table.component.ts"
must_contain: ["@Component", "ngOnInit", "selector"]
description: "Angular table component"
styling:
tailwind:
- path: "components/TailwindTable.tsx"
must_contain: ["className", "bg-", "border-", "rounded-"]
description: "Table component styled with Tailwind CSS utility classes"
css-modules:
- path: "components/Table.module.css"
must_contain: [".table", ".thead", ".tbody"]
description: "CSS module styles for table components"
- path: "components/StyledTable.tsx"
must_contain: ["import styles from", "styles.table"]
description: "Table component using CSS modules"
styled-components:
- path: "components/StyledTable.tsx"
must_contain: ["styled.", "const StyledTable", "css"]
description: "Table component using styled-components"
features:
sorting:
- path: "hooks/useSorting.ts"
must_contain: ["sort", "asc", "desc", "compareFn"]
description: "Hook for implementing sorting logic"
- path: "components/SortIcon.tsx"
must_contain: ["arrow", "direction", "sorted"]
description: "Visual indicator for sort direction"
filtering:
- path: "components/ColumnFilter.tsx"
must_contain: ["filter", "input", "onChange"]
description: "Per-column filter component"
- path: "components/FilterBar.tsx"
must_contain: ["filters", "apply", "reset"]
description: "Toolbar with filter controls"
selection:
- path: "hooks/useRowSelection.ts"
must_contain: ["selected", "toggle", "checkbox"]
description: "Hook for managing row selection state"
- path: "components/BulkActions.tsx"
must_contain: ["selected", "bulk", "delete", "export"]
description: "Toolbar for bulk operations on selected rows"
editing:
- path: "components/EditableCell.tsx"
must_contain: ["editable", "onUpdate", "validation"]
description: "Cell component with inline editing capability"
- path: "hooks/useTableEdit.ts"
must_contain: ["edit", "save", "cancel", "validation"]
description: "Hook for managing edit state and operations"
virtualization:
- path: "components/VirtualizedTable.tsx"
must_contain: ["useVirtualizer", "estimateSize", "overscan"]
description: "High-performance table with virtual scrolling"
- path: "hooks/useVirtualScroll.ts"
must_contain: ["virtualizer", "scrollElement", "getVirtualItems"]
description: "Hook for virtual scrolling functionality"
export:
- path: "utils/exportToCSV.ts"
must_contain: ["csv", "download", "blob", "stringify"]
description: "Utility to export table data as CSV"
- path: "utils/exportToExcel.ts"
must_contain: ["xlsx", "workbook", "export"]
description: "Utility to export table data as Excel"
responsive:
- path: "components/ResponsiveTable.tsx"
must_contain: ["@media", "mobile", "card", "breakpoint"]
description: "Table that transforms to cards on mobile"
- path: "hooks/useResponsiveColumns.ts"
must_contain: ["visibility", "priority", "hidden"]
description: "Hook for showing/hiding columns based on screen size"
scaffolding:
- path: "package.json"
reason: "Install TanStack Table, React Virtual, and related dependencies"
- path: "tsconfig.json"
reason: "Ensure TypeScript configuration supports table library types"
- path: "__tests__/Table.test.tsx"
reason: "Unit tests for table component functionality"
- path: "stories/Table.stories.tsx"
reason: "Storybook stories for table component variations"
- path: "README.md"
reason: "Documentation on table usage, features, and examples"
metadata:
primary_blueprints: ["dashboard", "crud-api", "frontend"]
secondary_blueprints: ["data-pipeline", "observability"]
contributes_to:
- "Data table components"
- "Sorting and filtering functionality"
- "Pagination controls"
- "Virtual scrolling for large datasets"
- "Row selection and bulk actions"
- "Inline editing interfaces"
- "Data export (CSV/Excel)"
- "Responsive table designs"
- "Server-side table operations"
- "Accessible ARIA grid patterns"
required_dependencies:
starter:
- "react@^18.0.0"
intermediate:
- "@tanstack/react-table@^8.10.0"
advanced:
- "@tanstack/react-table@^8.10.0"
- "@tanstack/react-virtual@^3.0.0"
optional_dependencies:
- "ag-grid-react@^31.0.0" # Enterprise grid solution
- "ag-grid-community@^31.0.0" # Community version
- "xlsx@^0.18.0" # Excel export
- "papaparse@^5.4.0" # CSV parsing
- "date-fns@^2.30.0" # Date formatting/sorting
performance_thresholds:
client_side_max: 1000 # Max rows for client-side operations
server_side_min: 1000 # Min rows to switch to server-side
virtualization_min: 10000 # Min rows to require virtualization
accessibility_requirements:
- "Semantic HTML table structure (table, thead, tbody, tr, td, th)"
- "ARIA grid pattern for interactive tables"
- "Keyboard navigation (arrow keys, Tab, Enter)"
- "Screen reader announcements for sort/filter changes"
- "Focus management for inline editing"
- "High contrast mode support"
testing_coverage:
- "Rendering with various data sizes"
- "Sorting (single and multi-column)"
- "Filtering (column and global)"
- "Pagination navigation"
- "Row selection (single and multi)"
- "Inline editing and validation"
- "Keyboard navigation"
- "Responsive behavior"
- "Performance benchmarks"
related_skills:
- "building-forms" # For inline editing
- "building-data-viz" # For data visualization in cells
- "design-tokens" # For theming and styling
- "implementing-apis" # For server-side operations
- "optimizing-sql" # For backend query optimization
Advanced Data Grid Features
Enterprise features: virtual scrolling, cell editing, aggregation, pivoting, and Excel export.
Table of Contents
- Virtual Scrolling (Large Datasets)
- Cell Editing (AG Grid)
- Aggregation (Group By)
- Pivot Tables
- Column Pinning
- Column Resizing
- Excel Export
- Context Menu
- Resources
Virtual Scrolling (Large Datasets)
Why: Render only visible rows for 100K+ row performance.
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualTable({ data }: { data: any[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Row height
overscan: 10, // Render extra rows above/below
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<TableRow data={data[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}Cell Editing (AG Grid)
import { AgGridReact } from 'ag-grid-react';
function EditableGrid() {
const [columnDefs] = useState([
{ field: 'name', editable: true },
{ field: 'email', editable: true },
{
field: 'status',
editable: true,
cellEditor: 'agSelectCellEditor',
cellEditorParams: { values: ['active', 'inactive'] },
},
]);
const onCellValueChanged = (params) => {
console.log('Cell changed:', params.data);
// Update database
updateUser(params.data.id, params.data);
};
return (
<AgGridReact
columnDefs={columnDefs}
rowData={data}
onCellValueChanged={onCellValueChanged}
singleClickEdit={true}
/>
);
}Aggregation (Group By)
// AG Grid with grouping
const columnDefs = [
{ field: 'category', rowGroup: true }, // Group by category
{ field: 'product' },
{ field: 'sales', aggFunc: 'sum' }, // Sum sales
{ field: 'units', aggFunc: 'avg' }, // Average units
];
<AgGridReact
columnDefs={columnDefs}
rowData={data}
groupDisplayType="multipleColumns"
autoGroupColumnDef={{ minWidth: 200 }}
/>Pivot Tables
const columnDefs = [
{ field: 'country', rowGroup: true },
{ field: 'year', pivot: true },
{ field: 'revenue', aggFunc: 'sum' },
];
// Produces:
// 2023 2024 2025
// USA $100K $150K $200K
// UK $80K $90K $110KColumn Pinning
const columnDefs = [
{ field: 'name', pinned: 'left', width: 200 }, // Always visible
{ field: 'email' },
{ field: 'phone' },
{ field: 'address' },
{ field: 'actions', pinned: 'right' }, // Always visible
];Column Resizing
import { useReactTable, getCoreRowModel } from '@tanstack/react-table';
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
columnResizeMode: 'onChange',
enableColumnResizing: true,
});
// Render resizable header
<th
style={{ width: header.getSize() }}
onMouseDown={header.getResizeHandler()}
>
{header.column.columnDef.header}
<div className="resizer" />
</th>Excel Export
import { utils, writeFile } from 'xlsx';
function exportToExcel(data: any[], filename: string) {
const worksheet = utils.json_to_sheet(data);
const workbook = utils.book_new();
utils.book_append_sheet(workbook, worksheet, 'Data');
writeFile(workbook, filename);
}
<button onClick={() => exportToExcel(tableData, 'export.xlsx')}>
Export to Excel
</button>Context Menu
function TableWithContextMenu({ data }: Props) {
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; rowId: number } | null>(null);
const handleContextMenu = (e: React.MouseEvent, rowId: number) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, rowId });
};
return (
<>
<table>
{data.map((row) => (
<tr key={row.id} onContextMenu={(e) => handleContextMenu(e, row.id)}>
<td>{row.name}</td>
</tr>
))}
</table>
{contextMenu && (
<div
style={{
position: 'fixed',
left: contextMenu.x,
top: contextMenu.y,
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
padding: '8px',
zIndex: 1000,
}}
>
<button onClick={() => handleEdit(contextMenu.rowId)}>Edit</button>
<button onClick={() => handleDelete(contextMenu.rowId)}>Delete</button>
</div>
)}
{contextMenu && (
<div
onClick={() => setContextMenu(null)}
style={{ position: 'fixed', inset: 0 }}
/>
)}
</>
);
}Resources
- TanStack Table: https://tanstack.com/table/
- AG Grid Enterprise: https://www.ag-grid.com/javascript-data-grid/
Basic Table Patterns
Foundational patterns for HTML tables, simple React tables, and accessibility compliance.
Table of Contents
- Semantic HTML Table
- Basic React Table
- Responsive Wrapper
- Empty State
- Loading State
- Accessibility
- Best Practices
- Sticky Header
- Resources
Semantic HTML Table
<table>
<caption>User List</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">John Doe</th>
<td>john@example.com</td>
<td>Active</td>
<td><button>Edit</button></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">Total: 156 users</td>
</tr>
</tfoot>
</table>Key elements:
<caption>- Table title for screen readersscope="col"- Column headersscope="row"- Row headers (first column)<tfoot>- Summary row
Basic React Table
interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
}
function BasicTable({ data }: { data: User[] }) {
return (
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="p-3 text-left">Name</th>
<th className="p-3 text-left">Email</th>
<th className="p-3 text-left">Status</th>
</tr>
</thead>
<tbody>
{data.map((user) => (
<tr key={user.id} className="border-b">
<td className="p-3">{user.name}</td>
<td className="p-3">{user.email}</td>
<td className="p-3">
<span className={user.status === 'active' ? 'text-green-600' : 'text-gray-400'}>
{user.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
);
}Responsive Wrapper
function ResponsiveTable({ children }: { children: React.ReactNode }) {
return (
<div className="overflow-x-auto">
<table className="min-w-full">
{children}
</table>
</div>
);
}Empty State
function TableWithEmpty({ data }: { data: any[] }) {
if (data.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<div className="text-4xl mb-4">📋</div>
<p>No data available</p>
</div>
);
}
return <BasicTable data={data} />;
}Loading State
function TableSkeleton({ rows = 5, cols = 4 }) {
return (
<table className="w-full">
<tbody>
{Array.from({ length: rows }).map((_, i) => (
<tr key={i}>
{Array.from({ length: cols }).map((_, j) => (
<td key={j} className="p-3">
<div className="h-4 bg-gray-200 rounded animate-pulse" />
</td>
))}
</tr>
))}
</tbody>
</table>
);
}Accessibility
<table
role="table"
aria-label="User data table"
aria-describedby="table-description"
>
<caption id="table-description">
List of users with name, email, and status
</caption>
{/* ... */}
</table>Best Practices
1. Use semantic HTML - <table>, <th>, <td>, not divs 2. Add caption - Describes table purpose 3. Scope attributes - scope="col" and scope="row" 4. Keyboard navigation - Tab through interactive cells 5. Responsive wrapper - Horizontal scroll on mobile 6. Loading states - Skeleton during fetch 7. Empty states - Clear message when no data 8. Stripe rows - Alternating background colors 9. Sticky header - Header visible during scroll 10. ARIA labels - For screen readers
Sticky Header
thead {
position: sticky;
top: 0;
background-color: white;
z-index: 10;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}Resources
- MDN Table: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table
- WCAG Tables: https://www.w3.org/WAI/tutorials/tables/
Related skills
FAQ
How does it decide the table implementation?
By data volume: simple HTML table under 100 rows, client-side features to 1,000, server-side to 10,000, and virtual scrolling above 10,000.
Which libraries does it recommend?
TanStack Table and AG Grid for interactive and enterprise-scale grids.