npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@talberos/custom-table

v1.0.17

Published

Advanced Excel-style table component for React with inline editing, cell selection, filters and more

Readme

@talberos/custom-table

Advanced Excel-style table component for React with inline editing, cell selection, filters, sorting and more.

npm version License: MIT


Table of Contents


Features

| 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 |


Installation

npm install @talberos/custom-table

Peer Dependencies

npm install react react-dom @mui/material @emotion/react @emotion/styled @tanstack/react-table

Quick Start

'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: '[email protected]', status: 'Active' },
  { id: 2, name: 'Jane Smith', email: '[email protected]', status: 'Pending' },
]

export default function MyTable() {
  return (
    <CustomTable
      data={data}
      columnsDef={columns}
      containerHeight="500px"
    />
  )
}

Component Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | data | any[] | required | Array of objects with data | | columnsDef | ColumnDef[] | required | Column definitions | | onCellUpdate | (rowId, colId, value) => Promise<void> | - | 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 |


Column Definition

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<void>
  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<string, { bg: string; text: string }>
  sortable?: boolean         // Allow sorting (default: true)
  filterable?: boolean       // Allow filtering (default: true)
  hidden?: boolean           // Hide column
  render?: (value: any, row: any) => React.ReactNode
}

Row Identifiers (Row IDs)

CustomTable automatically handles row identifiers to optimize performance and avoid conflicts with your business id field.

Automatic Detection

The table automatically detects which field to use as unique identifier following this priority order:

  1. Explicitly marked column with isRowId: true
  2. Column with accessorKey: 'id' (automatic detection)
  3. Auto-generated IDs based on index (fallback)

Use Cases

✅ Case 1: Using 'id' field from your database (RECOMMENDED)

If your data already has a unique id field, simply include it in the columns:

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: '[email protected]' },
  { id: 456, name: 'Jane Smith', email: '[email protected]' },
]

// ✅ 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.:

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:

const columns: ColumnDef[] = [
  { accessorKey: 'name', header: 'Name', editable: true },
  { accessorKey: 'email', header: 'Email', type: 'email' },
]

const data = [
  { name: 'John Doe', email: '[email protected]' },
  { name: 'Jane Smith', email: '[email protected]' },
]

// ✅ 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

Backend Persistence

When you edit a cell, onCellUpdate receives the row ID:

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
  ))
}

Best Practices

| Scenario | Recommendation | |----------|----------------| | API/Database data | ✅ Include id field in data and columns | | Different identifier | ✅ Use isRowId: true on corresponding column | | Static data without ID | ⚠️ Add sequential id before passing to table | | Multiple tables | ✅ Each table can have its own ID field |


Column Types

| 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 protected]" | | 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" |


Edit Types

| EditType | Description | |----------|-------------| | text | Free text (textarea) | | numeric | Numbers only with min/max | | select | Dropdown with search | | date | Date picker | | boolean | Visual toggle |


Supported Countries

67 countries with flags organized by region:

Americas (22)

Argentina, Bolivia, Brazil, Canada, Chile, Colombia, Costa Rica, Cuba, Ecuador, El Salvador, United States, Guatemala, Honduras, Mexico, Nicaragua, Panama, Paraguay, Peru, Puerto Rico, Dominican Republic, Uruguay, Venezuela

Europe (22)

Germany, Austria, Belgium, Denmark, Spain, Finland, France, Greece, Hungary, Ireland, Italy, Norway, Netherlands, Poland, Portugal, United Kingdom, Czech Republic, Romania, Russia, Sweden, Switzerland, Ukraine

Asia (17)

Saudi Arabia, China, South Korea, United Arab Emirates, Philippines, Hong Kong, India, Indonesia, Israel, Japan, Malaysia, Pakistan, Singapore, Thailand, Taiwan, Turkey, Vietnam

Africa and Oceania (6)

Australia, Egypt, Morocco, Nigeria, New Zealand, South Africa

{
  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
  ]
}

Practical Examples

Static Data (JSON)

'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 (
    <CustomTable
      data={products}
      columnsDef={columns}
      containerHeight="600px"
      rowHeight={32}
    />
  )
}

Fetch from REST API

'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 <div>Loading...</div>

  return (
    <CustomTable
      data={users}
      columnsDef={columns}
      onCellUpdate={handleCellUpdate}
      containerHeight="500px"
    />
  )
}

Editing and Persistence

const handleCellUpdate = async (rowId: string, colId: string, value: string) => {
  try {
    // 1. Save to backend
    await fetch(`/api/items/${rowId}`, {
      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)
  }
}

Dynamic Dropdowns

Basic Select

{
  accessorKey: 'status',
  header: 'Status',
  type: 'badge',
  editable: true,
  editType: 'select',
  options: [
    { value: 'Active', label: 'Active' },
    { value: 'Inactive', label: 'Inactive' },
  ]
}

Select with Dynamic Creation

{
  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 }])
  }
}

Context Menu

Right-click on table:

| Option | Description | |--------|-------------| | Copy | Copy selected cells | | Hide column | Hide the column | | Hide row | Hide the row |


Data Export

Default

Table includes automatic CSV export button.

Custom

const handleExportCSV = (data: any[]) => {
  // Your custom logic
}

<CustomTable
  data={data}
  columnsDef={columns}
  onExportCSV={handleExportCSV}
/>

Customization

Badge Colors

{
  accessorKey: 'priority',
  header: 'Priority',
  type: 'badge',
  badgeColors: {
    'High': { bg: '#FEE2E2', text: '#DC2626' },
    'Medium': { bg: '#FEF3C7', text: '#D97706' },
    'Low': { bg: '#D1FAE5', text: '#059669' },
  }
}

Custom Rendering

{
  accessorKey: 'actions',
  header: 'Actions',
  render: (value, row) => (
    <button onClick={() => handleEdit(row.id)}>Edit</button>
  )
}

Compact Rows

<CustomTable
  data={data}
  columnsDef={columns}
  rowHeight={28}
  defaultPageSize={100}
/>

Keyboard Shortcuts

| 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 |


API Reference

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'

Requirements

  • React 18+
  • @mui/material 5+
  • @tanstack/react-table 8+

Column Filters

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 |


Project Structure

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

License

MIT


Author

Gabriel Hércules Miguel


Repository

https://github.com/gabrielmiguelok/customtable