Advanced Excel-style table component for React with inline editing, cell selection, filters and more
npm install @talberos/custom-tableAdvanced Excel-style table component for React with inline editing, cell selection, filters, sorting and more.


---
- Features
- Installation
- Quick Start
- Component Props
- Column Definition
- Row Identifiers
- Column Types
- Edit Types
- Supported Countries
- Practical Examples
- Editing and Persistence
- Dynamic Dropdowns
- Context Menu
- Data Export
- Customization
- Keyboard Shortcuts
- API Reference
---
| Feature | Description |
|---------|-------------|
| Excel-like Selection | Click, drag, Shift+Click for ranges |
| Inline Editing | Double click to edit any cell |
| 15 Column Types | text, numeric, badge, country, date, datetime, link, email, phone, boolean, rating, progress, heatmap, sparkline, avatar |
| 5 Edit Types | text, numeric, select, date, boolean |
| Smart Dropdowns | With search, flags and dynamic creation |
| 67 Countries with Flags | Americas, Europe, Asia, Africa and Oceania |
| Auto-scroll | Automatic scroll when selecting near edges |
| Resizable Columns | Drag column borders to resize |
| Sorting | Click headers to sort ASC/DESC |
| Global Filter | Search across all columns |
| Pagination | 50, 100, 200, 500 or all rows |
| Context Menu | Right-click to copy, hide columns/rows |
| Copy Cells | Ctrl/Cmd + C to copy selection |
| Export | CSV and Excel |
| Themes | Automatic dark/light mode |
| Mobile-first | Optimized for touch |
| TypeScript | Full typing |
---
``bash`
npm install @talberos/custom-table
`bash`
npm install react react-dom @mui/material @emotion/react @emotion/styled @tanstack/react-table
---
`tsx
'use client'
import CustomTable from '@talberos/custom-table'
import type { ColumnDef } from '@talberos/custom-table'
const columns: ColumnDef[] = [
{ accessorKey: 'id', header: 'ID', width: 60 },
{ accessorKey: 'name', header: 'Name', editable: true },
{ accessorKey: 'email', header: 'Email', type: 'email' },
{ accessorKey: 'status', header: 'Status', type: 'badge' },
]
const data = [
{ id: 1, name: 'John Doe', email: 'john@mail.com', status: 'Active' },
{ id: 2, name: 'Jane Smith', email: 'jane@mail.com', status: 'Pending' },
]
export default function MyTable() {
return (
columnsDef={columns}
containerHeight="500px"
/>
)
}
`
---
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| data | any[] | required | Array of objects with data |columnsDef
| | ColumnDef[] | required | Column definitions |onCellUpdate
| | (rowId, colId, value) => Promise | - | Callback when editing cell |onExportCSV
| | (data) => void | - | CSV export handler |onExportExcel
| | (data) => void | - | Excel export handler |loadingText
| | string | 'Loading data...' | Loading text |noResultsText
| | string | 'No results found' | No data text |enableFilters
| | boolean | true | Enable global filter |enableColumnFilters
| | boolean | true | Enable column filters |enableSorting
| | boolean | true | Enable sorting |enablePagination
| | boolean | true | Enable pagination |enableCellSelection
| | boolean | true | Enable selection |enableCellEditing
| | boolean | true | Enable editing |enableExport
| | boolean | true | Show export button |defaultPageSize
| | number | 100 | Rows per page |rowHeight
| | number | 36 | Row height (px) |containerHeight
| | string \| number | '80vh' | Container height |defaultTheme
| | 'light' \| 'dark' \| 'system' | 'light' | Default theme |className
| | string | - | Custom CSS class |style
| | CSSProperties | - | Inline styles |
---
`typescript`
interface ColumnDef {
accessorKey: string // Field key in data
header: string // Header text
type?: ColumnType // Render type
width?: number // Initial width (default: 150)
minWidth?: number // Minimum width (default: 50)
maxWidth?: number // Maximum width (default: 800)
editable?: boolean // Allow editing
editType?: EditType // 'text' | 'numeric' | 'select' | 'date' | 'boolean'
options?: SelectOption[] // Options for select
allowCreate?: boolean // Create new options in select
onCreateOption?: (value: string) => Promise
isNumeric?: boolean // Indicates numeric (auto-detected if type: 'numeric')
isRowId?: boolean // Mark this column as unique row identifier
precision?: number // Decimals for numeric
min?: number // Minimum value for numeric
max?: number // Maximum value for numeric
textAlign?: 'left' | 'center' | 'right'
badgeColors?: Record
sortable?: boolean // Allow sorting (default: true)
filterable?: boolean // Allow filtering (default: true)
hidden?: boolean // Hide column
render?: (value: any, row: any) => React.ReactNode
}
---
CustomTable automatically handles row identifiers to optimize performance and avoid conflicts with your business id field.
The table automatically detects which field to use as unique identifier following this priority order:
1. Explicitly marked column with isRowId: trueaccessorKey: 'id'
2. Column with (automatic detection)
3. Auto-generated IDs based on index (fallback)
#### ✅ Case 1: Using 'id' field from your database (RECOMMENDED)
If your data already has a unique id field, simply include it in the columns:
`tsx
const columns: ColumnDef[] = [
{ accessorKey: 'id', header: 'ID', width: 80 }, // ← Automatically detected
{ accessorKey: 'name', header: 'Name', editable: true },
{ accessorKey: 'email', header: 'Email', type: 'email' },
]
const data = [
{ id: 123, name: 'John Doe', email: 'john@mail.com' },
{ id: 456, name: 'Jane Smith', email: 'jane@mail.com' },
]
// ✅ Table automatically uses 'id' field as internal identifier
// ✅ You can edit, filter and sort by 'id' without issues
// ✅ onCellUpdate will receive rowId='123' when editing first row
`
#### ✅ Case 2: ID field with different name
If your identifier is called userId, productId, etc.:
`tsx
const columns: ColumnDef[] = [
{ accessorKey: 'userId', header: 'User ID', width: 80, isRowId: true }, // ← Mark explicitly
{ accessorKey: 'name', header: 'Name', editable: true },
]
const data = [
{ userId: 'usr_123', name: 'John Doe' },
{ userId: 'usr_456', name: 'Jane Smith' },
]
// ✅ Table uses 'userId' as internal identifier
// ✅ onCellUpdate will receive rowId='usr_123' when editing first row
`
#### ✅ Case 3: Data without unique identifier
If your data doesn't have an ID field:
`tsx
const columns: ColumnDef[] = [
{ accessorKey: 'name', header: 'Name', editable: true },
{ accessorKey: 'email', header: 'Email', type: 'email' },
]
const data = [
{ name: 'John Doe', email: 'john@mail.com' },
{ name: 'Jane Smith', email: 'jane@mail.com' },
]
// ✅ Table generates IDs automatically: '0', '1', '2', etc.
// ⚠️ onCellUpdate will receive rowId='0' for first row
// ⚠️ NOT recommended if you need to sync with backend
`
When you edit a cell, onCellUpdate receives the row ID:
`tsx
const handleCellUpdate = async (rowId: string, colId: string, value: string) => {
// rowId is the value of the field marked as ID
// If using 'id' → rowId will be '123', '456', etc.
// If using 'userId' with isRowId: true → rowId will be 'usr_123', 'usr_456', etc.
// If no ID → rowId will be '0', '1', '2', etc. (index)
await fetch(/api/items/${rowId}, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [colId]: value })
})
// Update local state
setData(prev => prev.map(item =>
item.id === Number(rowId) ? { ...item, [colId]: value } : item
))
}
`
| Scenario | Recommendation |
|----------|----------------|
| API/Database data | ✅ Include id field in data and columns |isRowId: true
| Different identifier | ✅ Use on corresponding column |id
| Static data without ID | ⚠️ Add sequential before passing to table |
| Multiple tables | ✅ Each table can have its own ID field |
---
| Type | Description | Example |
|------|-------------|---------|
| text | Plain text (default) | "Hello world" |numeric
| | Formatted number | 1234.56 |badge
| | Colored label | "Active" |country
| | Flag + name | "Argentina" |date
| | Date | "2024-01-15" |datetime
| | Date and time | "2024-01-15T10:30" |link
| | Clickable URL | "https://..." |email
| | Email with mailto | "email@mail.com" |phone
| | Phone with tel: | "+1 234 5678" |boolean
| | Visual indicator | true / false |rating
| | Stars (1-5) | 4 |progress
| | Progress bar | 75 |heatmap
| | Color by value | 50 |sparkline
| | Mini chart | [10, 20, 15] |avatar
| | Circular image | "https://...jpg" |
---
| EditType | Description |
|----------|-------------|
| text | Free text (textarea) |numeric
| | Numbers only with min/max |select
| | Dropdown with search |date
| | Date picker |boolean
| | Visual toggle |
---
67 countries with flags organized by region:
`tsx`
{
accessorKey: 'country',
header: 'Country',
type: 'country',
editable: true,
editType: 'select',
options: [
{ value: 'Argentina', label: 'Argentina' },
{ value: 'Mexico', label: 'Mexico' },
{ value: 'Spain', label: 'Spain' },
// ... use exactly these names
]
}
---
`tsx
'use client'
import CustomTable from '@talberos/custom-table'
import type { ColumnDef } from '@talberos/custom-table'
const products = [
{ id: 1, name: 'Laptop', price: 999.99, stock: 15, status: 'Available' },
{ id: 2, name: 'Mouse', price: 29.99, stock: 150, status: 'Available' },
{ id: 3, name: 'Keyboard', price: 79.99, stock: 0, status: 'Out of Stock' },
]
const columns: ColumnDef[] = [
{ accessorKey: 'id', header: 'ID', width: 60 },
{ accessorKey: 'name', header: 'Product', editable: true },
{ accessorKey: 'price', header: 'Price', type: 'numeric', precision: 2 },
{ accessorKey: 'stock', header: 'Stock', type: 'numeric' },
{
accessorKey: 'status',
header: 'Status',
type: 'badge',
badgeColors: {
'Available': { bg: '#D1FAE5', text: '#059669' },
'Out of Stock': { bg: '#FEE2E2', text: '#DC2626' },
}
},
]
export default function ProductsTable() {
return (
columnsDef={columns}
containerHeight="600px"
rowHeight={32}
/>
)
}
`
`tsx
'use client'
import { useState, useEffect } from 'react'
import CustomTable from '@talberos/custom-table'
import type { ColumnDef } from '@talberos/custom-table'
const columns: ColumnDef[] = [
{ accessorKey: 'id', header: 'ID', width: 60 },
{ accessorKey: 'name', header: 'Name', editable: true },
{ accessorKey: 'email', header: 'Email', type: 'email' },
{ accessorKey: 'phone', header: 'Phone', type: 'phone' },
]
export default function UsersTable() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
}, [])
const handleCellUpdate = async (rowId: string, colId: string, value: string) => {
await fetch(/api/users/${rowId}, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [colId]: value })
})
setUsers(prev =>
prev.map(u => u.id === Number(rowId) ? { ...u, [colId]: value } : u)
)
}
if (loading) return
return (
columnsDef={columns}
onCellUpdate={handleCellUpdate}
containerHeight="500px"
/>
)
}
`
---
`tsx/api/items/${rowId}
const handleCellUpdate = async (rowId: string, colId: string, value: string) => {
try {
// 1. Save to backend
await fetch(, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [colId]: value })
})
// 2. Update local state
setData(prev => prev.map(item =>
item.id === Number(rowId) ? { ...item, [colId]: value } : item
))
} catch (error) {
console.error('Error:', error)
}
}
`
---
`tsx`
{
accessorKey: 'status',
header: 'Status',
type: 'badge',
editable: true,
editType: 'select',
options: [
{ value: 'Active', label: 'Active' },
{ value: 'Inactive', label: 'Inactive' },
]
}
`tsx`
{
accessorKey: 'category',
header: 'Category',
type: 'badge',
editable: true,
editType: 'select',
options: categories,
allowCreate: true,
onCreateOption: async (newValue) => {
await fetch('/api/categories', { method: 'POST', body: JSON.stringify({ name: newValue }) })
setCategories(prev => [...prev, { value: newValue, label: newValue }])
}
}
---
Right-click on table:
| Option | Description |
|--------|-------------|
| Copy | Copy selected cells |
| Hide column | Hide the column |
| Hide row | Hide the row |
---
Table includes automatic CSV export button.
`tsx
const handleExportCSV = (data: any[]) => {
// Your custom logic
}
columnsDef={columns}
onExportCSV={handleExportCSV}
/>
`
---
`tsx`
{
accessorKey: 'priority',
header: 'Priority',
type: 'badge',
badgeColors: {
'High': { bg: '#FEE2E2', text: '#DC2626' },
'Medium': { bg: '#FEF3C7', text: '#D97706' },
'Low': { bg: '#D1FAE5', text: '#059669' },
}
}
`tsx`
{
accessorKey: 'actions',
header: 'Actions',
render: (value, row) => (
)
}
`tsx`
columnsDef={columns}
rowHeight={28}
defaultPageSize={100}
/>
---
| Key | Action |
|-----|--------|
| ↑ ↓ ← → | Navigate between cells |Shift + Arrows
| | Extend selection |Ctrl/Cmd + C
| | Copy cells |Enter
| | Confirm edit |Tab
| | Next cell |Escape
| | Cancel edit |Double click
| | Start editing |
---
`tsx`
import CustomTable, {
type ColumnDef,
type CustomTableProps,
type ColumnType,
type EditType,
type SelectOption,
buildColumns,
THEME_COLORS,
COLUMN_CONFIG,
STYLES_CONFIG,
TableEditContext,
useTableEdit,
} from '@talberos/custom-table'
---
- React 18+
- @mui/material 5+
- @tanstack/react-table 8+
---
Filters are automatically shown below each header according to column type:
| Column Type | Filter Type |
|-------------|-------------|
| text, email, phone, link, country | Text input |numeric
| , rating, progress, heatmap | Min-Max range |badge
| (with options) | Options dropdown |boolean
| | Yes/No dropdown |date
| , datetime | Date picker |
---
```
src/
├── index.tsx # Main CustomTable component
├── types.ts # TypeScript types
├── config.ts # Configuration (sizes, styles)
├── CustomTableColumnsConfig.tsx # Column rendering and flags
├── theme/
│ └── colors.ts # Theme colors
├── hooks/
│ ├── useThemeMode.ts # Light/dark theme handling
│ ├── useCustomTableLogic.ts # Export logic
│ └── useCellEditingOrchestration.ts # Edit orchestration
└── TableView/
├── index.tsx # Main table view
├── hooks/
│ ├── useCellSelection.ts # Cell selection and auto-scroll
│ ├── useColumnResize.ts # Resize columns
│ ├── useInlineCellEdit.ts # Inline editing
│ ├── useClipboardCopy.ts # Copy to clipboard
│ └── useTableViewContextMenu.ts # Context menu
├── logic/
│ ├── domUtils.ts # DOM utilities
│ ├── selectionLogic.ts # Selection logic
│ └── dragLogic.ts # Drag logic
└── subcomponents/
├── TableHeader.tsx # Headers and filters
├── TableBody.tsx # Table body
├── Pagination.tsx # Pagination
├── ContextualMenu.tsx # Context menu
├── CustomSelectDropdown.tsx # Custom dropdown
├── LoadingOverlay.tsx # Loading overlay
└── NoResultsOverlay.tsx # No results overlay
---
MIT
---
Gabriel Hércules Miguel
- LinkedIn: gabrielherculesmiguel
- GitHub: @gabrielmiguelok
---