@c-time/simple-ex-grid
v1.4.0
Published
Excel-like grid component for React with clipboard, touch support, and cell selection
Readme
@c-time/simple-ex-grid
Excel-like grid component for React. Touch-first design with clipboard support, drag selection, and three operating modes.
Install
npm install @c-time/simple-ex-grid
# or
pnpm add @c-time/simple-ex-gridPeer dependencies: React 19+
Quick Start
import { SimpleExGrid } from '@c-time/simple-ex-grid'
import type { ColumnDef } from '@c-time/simple-ex-grid'
interface Row {
id: number
name: string
price: number
}
const columns: ColumnDef<Row>[] = [
{ key: 'id', header: 'ID', width: 60, editable: false },
{ key: 'name', header: 'Name', width: 200 },
{ key: 'price', header: 'Price', width: 120, parser: (s) => Number(s) || 0 },
]
function App() {
const [rows, setRows] = useState<Row[]>([
{ id: 1, name: 'Item A', price: 100 },
{ id: 2, name: 'Item B', price: 200 },
])
return (
<SimpleExGrid
rows={rows}
columns={columns}
mode="edit"
onChange={setRows}
/>
)
}Modes
| Mode | Description |
|------|-------------|
| select (default) | Selection and copy/paste only. No cell editing. |
| edit | Re-tap active cell or press Enter/F2 to start editing. |
| readonly | Selection and copy only. All mutations disabled. |
Props
Data
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| rows | Row[] | required | Row data array |
| columns | ColumnDef<Row>[] | required | Column definitions |
| onChange | (rows, changes) => void | - | Called when cell values change |
Mode & Display
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| mode | 'select' \| 'edit' \| 'readonly' | 'select' | Operating mode |
| showRowGripHeader | boolean | true | Show row grip headers (left) |
| showColumnGripHeader | boolean | false | Show column grip headers (top) |
| rowHeight | 'single-line' \| 'fit-content' | 'single-line' | single-line: truncate with ellipsis. fit-content: wrap text |
Column Definition
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| key | keyof Row & string | required | Row property key |
| header | string | required | Header display text |
| width | number \| '${n}%' \| 'fit-content' | 120 | Column width. Fixed px, percentage, or auto |
| editable | boolean | true | Set false to make column read-only |
| formatter | (value, row) => string | - | Display formatter |
| parser | (input) => unknown | - | Input parser (string to value) |
| validator | (value) => string \| null | - | Validation (return error message or null) |
| sortMark | 'asc' \| 'desc' \| null | null | Display sort indicator (▲/▼) in header. Display only — sorting logic is the caller's responsibility. |
| align | 'left' \| 'center' \| 'right' | - | Default text alignment for the column |
| headerAlign | 'left' \| 'center' \| 'right' | - | Header text alignment (overrides align) |
| color | string | - | Default text color for the column (any CSS color) |
| fontStyle | 'normal' \| 'italic' | - | Default font style for the column |
| fontWeight | 'normal' \| 'bold' | - | Default font weight for the column |
| hidden | boolean | false | Hide column visually. Hidden columns are still included in clipboard operations. |
| render | (value, row, rowIndex) => ReactNode | - | Custom cell renderer. Returned node replaces default text display. |
Pagination
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| pagination | boolean | false | Enable pagination UI |
| pageSize | number | 20 | Rows per page |
| page | number | - | Controlled current page (0-indexed) |
| onPageChange | (page) => void | - | Page change callback |
| onPageSizeChange | (pageSize) => void | - | Page size change callback |
Selection
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| selection | CellRange \| null | - | Controlled selection range |
| onSelectionChange | (range) => void | - | Selection change callback |
| onActiveCellChange | (row, col) => void | - | Active cell move callback |
Clipboard
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| clipboard.enabled | boolean | true | Enable clipboard operations |
| clipboard.format | 'tsv' \| 'html' \| 'both' | 'both' | Clipboard data format |
Keyboard shortcuts: Ctrl+C copy, Ctrl+X cut, Ctrl+V insert paste (add rows), Ctrl+Shift+V overwrite paste.
Cell Layout
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| colSpan | (row, col) => number | - | Return column span for a cell. Spanned cells are merged visually. |
| cellAlign | (row, col) => 'left' \| 'center' \| 'right' \| undefined | - | Per-cell text alignment override (takes priority over column align). |
| cellColor | (row, col) => string \| undefined | - | Per-cell text color override (takes priority over column color). |
| cellFontStyle | (row, col) => 'normal' \| 'italic' \| undefined | - | Per-cell font style override (takes priority over column fontStyle). |
| cellFontWeight | (row, col) => 'normal' \| 'bold' \| undefined | - | Per-cell font weight override (takes priority over column fontWeight). |
Cell Badge
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| cellBadge | (row, col) => CellBadge \| undefined | - | Show a colored dot on the cell's top-right corner. Hover/tap to display a tooltip. |
CellBadge has two properties: type ('success' / 'warning' / 'error') and message (tooltip text).
| Type | Color | Use case |
|------|-------|----------|
| success | Green (#27ae60) | Approved, valid, complete |
| warning | Orange (#f39c12) | Caution, approaching limit |
| error | Red (#e74c3c) | Validation error, over limit |
<SimpleExGrid
rows={rows}
columns={columns}
cellBadge={(row, col) => {
if (col === 2 && rows[row].price >= 10000)
return { type: 'success', message: '承認済み' }
if (col === 3 && rows[row].qty >= 200)
return { type: 'error', message: `上限超過: ${rows[row].qty}` }
}}
/>Badge values are display-only and not included in clipboard operations.
Row Mutation
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| allowRowAdd | boolean | false | Allow adding rows (paste auto-extend, insert paste) |
| allowRowDelete | boolean | false | Allow deleting rows (context menu) |
| onRowsAdd | (startRow, count) => void | - | Fires after rows are added |
| onRowsDelete | (startRow, count) => void | - | Fires after rows are deleted |
Undo / Redo
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| maxHistory | number | - | Maximum number of history entries to keep |
| shouldRecordHistory | (changes, rowEvent?) => boolean | - | Filter which mutations are recorded. rowEvent is { kind, startRow, count } for row add/delete. |
| historyApiRef | React.MutableRefObject<HistoryApi<Row>> | - | Ref to access undo/redo API externally |
HistoryApi<Row> exposes undo(), redo(), canUndo, canRedo, and pushHistory(before, after).
Keyboard shortcuts: Ctrl+Z undo, Ctrl+Y / Ctrl+Shift+Z redo.
Context Menu
| Prop | Type | Description |
|------|------|-------------|
| contextMenuItems | (defaultItems, context) => ContextMenuItem[] | Customize right-click menu items. Receives built-in items and context info, returns the final menu items. |
ContextMenuContext provides cell ({ row, col } | null), selection (CellRange | null), and mode.
<SimpleExGrid
rows={rows}
columns={columns}
contextMenuItems={(defaultItems, { cell }) => [
...defaultItems,
{ type: 'separator' },
{ label: '行を複製', disabled: !cell, onClick: () => duplicateRow(cell!.row) },
]}
/>Editing Lifecycle
| Prop | Type | Description |
|------|------|-------------|
| onEditStart | (row, col) => boolean \| void | Fires when editing begins. Return false to cancel. |
| onEditCommit | (row, col, before, after) => boolean \| void | Fires on edit confirm. Return false to prevent write. |
| onEditCancel | (row, col) => void | Fires when editing is cancelled (Escape). |
Clipboard Lifecycle
| Prop | Type | Description |
|------|------|-------------|
| onBeforeCopy | (context: BeforeCopyContext) => string[][] \| false \| void | Fires before copy/cut. Return false to cancel, string[][] to replace data, or void to proceed. |
| onBeforePaste | (context: BeforePasteContext) => string[][] \| false \| void | Fires before paste. Return false to cancel, string[][] to replace data, or void to proceed. |
BeforeCopyContext provides data (2D string array), source ({ row, col, endRow, endCol }), cut (boolean), and a mutable metadata (Record<string, unknown>) for attaching custom data to the clipboard.
BeforePasteContext provides data (2D string array), target ({ row, col }), insert (boolean — true for insert-paste, false for overwrite-paste), and metadata (ClipboardMeta | null — null when pasted from an external source).
Clipboard Metadata
When copying from the grid, a ClipboardMeta object is automatically written to the clipboard alongside the TSV/HTML data using a custom MIME type (web application/x-seg-clipboard). This metadata is available in onBeforePaste when pasting back into the same or another grid instance.
ClipboardMeta contains:
source— the original copy range ({ row, col, endRow, endCol })columnKeys— column keys of the copied rangecolSpans— per-row colspan values for owner columnscustom— user-defined metadata set viaonBeforeCopy'scontext.metadata
<SimpleExGrid
rows={rows}
columns={columns}
onBeforeCopy={(ctx) => {
// Attach custom metadata to the clipboard
ctx.metadata.sourceSheet = 'Sheet1'
ctx.metadata.copiedAt = Date.now()
}}
onBeforePaste={(ctx) => {
if (ctx.metadata) {
console.log('Pasted from grid:', ctx.metadata.custom.sourceSheet)
console.log('Column keys:', ctx.metadata.columnKeys)
} else {
console.log('Pasted from external source')
}
}}
/>Features
Selection
- Click to select, Shift+Click to extend range, drag for rectangle selection
- Arrow keys to navigate, Tab to move right (wraps), Shift+Tab to move left
Ctrl+Ato select all,Escapeto clear selection- Grip headers: click to select entire row/column, drag to extend
Editing (edit mode)
- Re-tap active cell, press Enter, F2, or type a character to begin editing
- Enter commits and moves down, Tab commits and moves right, Escape cancels
- Double-click also starts editing in any writable mode
Clipboard
- Copy/cut/paste with Excel-compatible TSV + HTML table format
- Paste auto-extends rows when
allowRowAddis enabled - Insert paste (
Ctrl+V) inserts new rows at selection position - Overwrite paste (
Ctrl+Shift+V) overwrites existing cells Ctrl+Iinsert rows,Ctrl+Ddelete selected rows- Right-click context menu for all clipboard and row operations
- Undo/redo (
Ctrl+Z/Ctrl+Y) tracks all cell edits, paste, and row operations
Touch Support
- Pointer Events API for unified mouse/touch/pen handling
setPointerCapturefor reliable touch drag selection- Long-press opens context menu without losing selection (deferred selection pattern)
Layout
- Sticky grip headers (row headers stick left, column headers stick top)
- Custom overlay scrollbars (no layout shift from native scrollbar appearance)
- Works in fixed-size containers, percentage-based layouts, and unconstrained layouts
- Flex-based scroll structure handles both constrained and auto-sized parents
Development
pnpm install
pnpm dev # Storybook dev server
pnpm test # Vitest
pnpm typecheck # TypeScript
pnpm lint # ESLint
pnpm build # Vite library buildLicense
MIT
