A lightweight, Excel-like editable grid component for React.
- Excel-like cell selection (click & drag)
- Keyboard navigation (Arrow keys to move, Shift+Arrow to extend selection)
- Copy/Paste support (Ctrl+C / Ctrl+V)
- Auto Fill with arithmetic sequence detection (drag fill handle)
- Grouped column headers with rowSpan support
- Grouped row headers with rowSpan support
- Click outside to clear selection
- Double-click or type to edit cells
- Expandable input field when editing (text overflow handling)
- Keyboard shortcuts (Delete/Backspace to clear)
- Styling-agnostic: Works with Tailwind CSS, CSS Modules, plain CSS, or any styling solution
- Zero external dependencies
npm install react-excel-liteimport { useState } from "react";
import { ExcelGrid } from "react-excel-lite";
function App() {
const [data, setData] = useState([
["100", "200", "300"],
["400", "500", "600"],
["700", "800", "900"],
]);
return <ExcelGrid data={data} onChange={setData} />;
}| Prop | Type | Required | Description |
|---|---|---|---|
data |
string[][] |
Yes | 2D array of strings |
onChange |
(data: string[][]) => void |
Yes | Callback when data changes |
rowHeaders |
HeaderGroup[] |
No | Grouped row headers |
colHeaders |
HeaderGroup[] |
No | Grouped column headers |
className |
string |
No | CSS class for container |
rowHeaderTitle |
string |
No | Title for row header column |
styles |
GridStyles |
No | Style configuration object |
cellStyles |
(coord: CellCoord) => string|undefined |
No | Function to style individual cells |
import { useState } from "react";
import { ExcelGrid } from "react-excel-lite";
import type { HeaderGroup } from "react-excel-lite";
function App() {
const [data, setData] = useState([
["100", "200", "300", "400"],
["500", "600", "700", "800"],
]);
const colHeaders: HeaderGroup[] = [
{
label: "Q1",
description: "First quarter",
headers: [
{ key: "jan", label: "Jan", description: "January" },
{ key: "feb", label: "Feb", description: "February" },
],
},
{
label: "Q2",
description: "Second quarter",
headers: [
{ key: "mar", label: "Mar", description: "March" },
{ key: "apr", label: "Apr", description: "April" },
],
},
];
const rowHeaders: HeaderGroup[] = [
{
label: "Products",
description: "Product categories",
headers: [
{ key: "prodA", label: "Product A", description: "Main product line" },
{ key: "prodB", label: "Product B", description: "Secondary product" },
],
},
];
return (
<ExcelGrid
data={data}
onChange={setData}
colHeaders={colHeaders}
rowHeaders={rowHeaders}
rowHeaderTitle="Category"
/>
);
}When all HeaderGroups have no label, the grid displays a single header row/column instead of two levels:
const colHeaders: HeaderGroup[] = [
{
// No label - single header row
headers: [
{ key: "jan", label: "Jan" },
{ key: "feb", label: "Feb" },
],
},
{
headers: [
{ key: "mar", label: "Mar" },
{ key: "apr", label: "Apr" },
],
},
];
const rowHeaders: HeaderGroup[] = [
{
// No label - single header column
headers: [
{ key: "row1", label: "Row 1" },
{ key: "row2", label: "Row 2" },
],
},
];
<ExcelGrid
data={data}
onChange={setData}
colHeaders={colHeaders}
rowHeaders={rowHeaders}
/>;If at least one group has a label, the grid shows the two-level layout (group labels + individual headers).
The component comes with sensible default styles built-in. You can customize styles using the styles prop with CSS class strings from any styling solution.
Out of the box, the grid has:
- Light gray borders and headers
- Blue selection highlight
- Blue fill handle
import type { GridStyles } from "react-excel-lite";
const styles: GridStyles = {
cell: "text-sm",
selected: "bg-purple-100 ring-2 ring-inset ring-purple-500",
fillTarget: "bg-purple-50",
fillHandle: "bg-purple-500",
colGroup: "bg-purple-100 text-purple-700",
colHeader: "bg-purple-50",
rowHeader: "bg-slate-200",
};
<ExcelGrid data={data} onChange={setData} styles={styles} />;import styles from "./grid.module.css";
import type { GridStyles } from "react-excel-lite";
const gridStyles: GridStyles = {
selected: styles.selectedCell,
fillTarget: styles.fillTargetCell,
fillHandle: styles.fillHandle,
};
<ExcelGrid data={data} onChange={setData} styles={gridStyles} />;const styles: GridStyles = {
selected: "my-selected-cell",
fillTarget: "my-fill-target",
fillHandle: "my-fill-handle",
};
<ExcelGrid data={data} onChange={setData} styles={styles} />;/* styles.css */
.my-selected-cell {
background-color: #f3e8ff;
outline: 2px solid #a855f7;
outline-offset: -2px;
}
.my-fill-target {
background-color: #faf5ff;
}
.my-fill-handle {
background-color: #a855f7;
}interface GridStyles {
cell?: string; // CSS class for data cells
selected?: string; // CSS class for selected cells (overrides default)
fillTarget?: string; // CSS class for fill target cells (overrides default)
fillHandle?: string; // CSS class for fill handle (overrides default)
colGroup?: string; // CSS class for column group headers
colHeader?: string; // CSS class for individual column headers
rowHeader?: string; // CSS class for row headers
}Style individual column headers and groups:
const colHeaders: HeaderGroup[] = [
{
label: "Revenue",
className: "bg-green-100 text-green-700",
headers: [
{ key: "q1r", label: "Q1", className: "bg-green-50" },
{ key: "q2r", label: "Q2", className: "bg-green-50" },
],
},
];Style individual row headers:
const rowHeaders: HeaderGroup[] = [
{
label: "Regions",
className: "bg-slate-700 text-white",
headers: [
{ key: "regionA", label: "Region A", className: "bg-slate-600 text-white" },
{ key: "regionB", label: "Region B", className: "bg-slate-500 text-white" },
],
},
];Use the cellStyles prop to apply styles to specific cells based on their coordinates:
import { useCallback } from "react";
import type { CellCoord } from "react-excel-lite";
function App() {
const [data, setData] = useState([
["100", "200", "300"],
["400", "500", "600"],
["700", "800", "900"],
]);
// Memoize to prevent unnecessary re-renders
const cellStyles = useCallback((coord: CellCoord) => {
// Highlight first row
if (coord.row === 0) return "bg-yellow-100";
// Highlight specific cell
if (coord.row === 1 && coord.col === 1) return "bg-red-100 font-bold";
// Highlight cells with negative values (check data)
return undefined;
}, []);
return <ExcelGrid data={data} onChange={setData} cellStyles={cellStyles} />;
}Common use cases:
- Highlight header rows or columns
- Show validation errors (e.g., red background for invalid cells)
- Conditional formatting based on cell values
- Alternating row colors
// Alternating row colors
const cellStyles = useCallback((coord: CellCoord) => {
return coord.row % 2 === 0 ? "bg-gray-50" : "bg-white";
}, []);
// Value-based styling (check data in callback)
const cellStyles = useCallback((coord: CellCoord) => {
const value = Number(data[coord.row]?.[coord.col]);
if (value < 0) return "bg-red-100 text-red-700";
if (value > 1000) return "bg-green-100 text-green-700";
return undefined;
}, [data]);Select cells with a numeric pattern and drag the fill handle to auto-fill:
1, 2, 3→ drag down →4, 5, 6, 7, ...100, 200, 300→ drag down →400, 500, 600, ...10, 8, 6→ drag down →4, 2, 0, -2, ...- Text values → repeats the pattern
| Shortcut | Action |
|---|---|
Arrow Keys |
Move selection |
Shift + Arrow Keys |
Extend selection range |
Enter |
Enter edit mode (select all text) |
Any character |
Enter edit mode and start typing |
Escape |
Exit edit mode |
Ctrl+C / Cmd+C |
Copy selected cells |
Ctrl+V / Cmd+V |
Paste from clipboard |
Delete / Backspace |
Clear selected cells |
ExcelGrid- Main grid componentGridCell- Individual cell component
useGridSelection- Cell selection logicuseGridClipboard- Copy/paste and keyboard navigation logicuseGridDragFill- Fill handle logic
cn- Classname merge utilitycoordToKey- Convert coordinate to string keykeyToCoord- Convert string key to coordinategetCellsInRange- Get all cells in a rangeisCellInRange- Check if cell is in rangeparseTSV- Parse TSV string to 2D arraytoTSV- Convert 2D array to TSV stringnormalizeRange- Normalize selection rangegetFillTargetCells- Get fill target cells
interface CellCoord {
row: number;
col: number;
}
interface SelectionRange {
start: CellCoord | null;
end: CellCoord | null;
}
interface Header {
key: string;
label: string;
description?: string;
className?: string;
}
interface HeaderGroup {
label?: string;
headers: Header[];
description?: string;
className?: string;
}
interface GridStyles {
cell?: string;
selected?: string;
fillTarget?: string;
fillHandle?: string;
colGroup?: string;
colHeader?: string;
rowHeader?: string;
}
interface ExcelGridProps {
data: string[][];
onChange: (data: string[][]) => void;
rowHeaders?: HeaderGroup[];
colHeaders?: HeaderGroup[];
className?: string;
rowHeaderTitle?: string;
styles?: GridStyles;
cellStyles?: (coord: CellCoord) => string | undefined;
}MIT License © 2025 prkgnt