# React Editable Table: Spreadsheet Grid or Inline Edit

> Build a react editable table that works like a spreadsheet: typed cells, range selection, Excel copy and paste, undo, validation, or inline edit per cell.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Nov 5, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-editable-table

## TL;DR

A react editable table is either a data table with a few cells that edit in place and save one value at a time, or a spreadsheet-like grid with typed cells, range selection, Excel copy and paste, undo and validation. Use inline edit when people mostly read and occasionally fix a field. Use an editable grid when they enter or correct many rows in one sitting and expect Ctrl+V and Ctrl+Z to work like in Excel.

A react editable table sounds like one component, but people mean two different things by it. Sometimes they want a normal data table where a price or a status can be fixed in place. Sometimes they want a small spreadsheet inside the app: type over a cell, select a range, paste 40 rows from Excel, press Ctrl+Z when it goes wrong. Those are different builds with different costs. This guide shows both with Wingo UI components, the [Editable Grid](https://wingo-ui.com/components/editable-grid) for the spreadsheet case and [Inline Edit](https://wingo-ui.com/components/inline-edit) inside the [Data Table](https://wingo-ui.com/components/data-table) for the lighter one, and how to pick between them.

## Which kind of React editable table do you need?

Look at what people do on the screen most of the time. If they mostly read, search and filter, and edit a field now and then, a few editable cells in a normal data table are enough. If they enter or correct many rows in one sitting, they need a grid that behaves like a spreadsheet, because anything less makes them tab through inputs one by one.

| | Inline edit in a data table | Editable grid |
| --- | --- | --- |
| Main job | Browse, search, filter, fix one value | Enter and correct many rows |
| Edit model | Click a value, edit, save that value | Type over any cell, Enter and Tab move like Excel |
| Selection | Rows (checkboxes, bulk actions) | Cells and ranges, rows, columns |
| Clipboard | Copy text like any page | Copy, cut and paste blocks as tab separated values |
| Undo | Per field (Escape cancels) | Undo and redo across edits, pastes and deletes |
| Saving | One request per value, rollback on failure | Per cell, per batch or the whole sheet |
| Examples | CRM contacts, orders, user admin | Price lists, timesheets, budgets, imports |

## Does shadcn have an editable table?

It does not. The [shadcn Data Table page](https://ui.shadcn.com/docs/components/data-table) (checked November 5, 2026) says on purpose that it is a guide to building your own table on TanStack Table instead of a component. It covers sorting, filtering, pagination, column visibility and row selection. Cell editing is not part of it, so a "shadcn editable table" means writing the editors, the keyboard handling and the saving yourself in the cell renderer. That is fine for one editable column. It gets expensive once you need range selection and paste, which is where a dedicated react editable grid earns its place. If you want the TanStack side of that story, our [TanStack Table v9 data table walkthrough](https://wingo-ui.com/blog/tanstack-table-v9-data-table) builds the read side step by step.

## How do you build a spreadsheet-like editable grid in React?

You describe the columns with a type each and pass the rows. The Editable Grid handles the rest: the active cell, range selection, the editors, keyboard movement, clipboard, undo and validation. Install it with:

```bash
npx wingo-ui@latest add editable-grid
```

Every row needs a unique string `id`. Each column's `id` is the row key it edits, and `type` picks the editor and the parser: `text`, `number`, `currency`, `select`, `date` or `checkbox`.

```tsx
"use client"

import { useState } from "react"
import { EditableGrid, type GridColumn, type GridRow } from "@/components/ui/editable-grid"

type Product = GridRow & {
  name: string
  category: string
  price: number | null
  stock: number | null
  active: boolean
}

const columns: GridColumn<Product>[] = [
  { id: "name", header: "Product", required: true, width: 240 },
  {
    id: "category",
    header: "Category",
    type: "select",
    options: [
      { value: "licenses", label: "Licenses" },
      { value: "services", label: "Services" },
    ],
  },
  { id: "price", header: "Price before tax", type: "currency", currency: "USD", min: 0 },
  { id: "stock", header: "Stock", type: "number", step: 1, min: 0 },
  { id: "active", header: "Active", type: "checkbox" },
]

export function PriceList() {
  const [rows, setRows] = useState<Product[]>([
    { id: "p-1", name: "Business license (month)", category: "licenses", price: 49, stock: 120, active: true },
    { id: "p-2", name: "Setup and training (hour)", category: "services", price: 65, stock: null, active: true },
  ])

  return (
    <EditableGrid
      title="Price list 2026"
      columns={columns}
      value={rows}
      onValueChange={setRows}
      locale="en-US"
      createRow={() => ({ id: crypto.randomUUID(), name: "", category: "licenses", price: null, stock: null, active: true })}
    />
  )
}
```

The keyboard works the way spreadsheet users expect. Typing replaces the cell, Enter or F2 or a double click edits it. Enter commits and moves down, Tab commits and moves right, Escape cancels. Shift with the arrows, Shift+click or a mouse drag selects a range, a row number selects the row and a column header the column. The whole grid is one Tab stop, so keyboard users can still leave it with Tab when they are not editing.

> Live demo (Editable Grid): Click a price, type a new one and press Enter. Then Shift+click another cell to select a range, copy it with Ctrl+C or Cmd+C, and paste it into a spreadsheet. Try it at [Editable Grid](https://wingo-ui.com/components/editable-grid) and install it with `npx wingo-ui@latest add editable-grid`.

Above 100 rows (the `virtualThreshold` default) only the visible rows render, inside the scroller's `height` (480px by default; `height={0}` grows with the rows and never virtualizes). The docs include a 2,000 row example with a frame counter so you can check it on your own hardware. Columns are not virtualized, so the grid is meant for the under 30 columns a form-like sheet has. For read-only tables with 100k rows, our [React virtualized table guide](https://wingo-ui.com/blog/react-virtualized-table) covers the TanStack Virtual setup.

## How does Excel copy and paste work in a React grid?

Excel and Google Sheets put a copied range on the clipboard as plain text: one line per row, a tab between cells. The grid reads that on Ctrl+V, writes the block starting at the active cell and appends rows when the paste is taller than what is left (turn that off with `allowPasteNewRows={false}`). Ctrl+C does the reverse, so a range copied from the grid pastes straight into a spreadsheet.

The hard part is the conversion, not the splitting. A pasted "1,234.50" is 1234.5 in `en-US` but means something else in `de-DE`, and "10/06/2026" is a different day in the US than in Europe. The grid parses numbers, money and dates in its `locale`, matches option labels to their values, and reads "true", "yes" or "x" as a checked box. In number, money, date and select columns, text that does not parse stays in the cell as text and is flagged as invalid, so nothing gets lost silently. Both helpers are exported if you need them elsewhere, for example in a CSV import:

```ts
import { coerceCellValue, parseClipboardGrid } from "@/components/ui/editable-grid"

const cells = parseClipboardGrid("Label printer\t2\t1,249.00\n")
// [["Label printer", "2", "1,249.00"]]

coerceCellValue("1,249.00", { id: "price", header: "Price", type: "currency" }, "en-US")
// 1249
```

For whole files instead of clipboard blocks, the [CSV Import](https://wingo-ui.com/components/csv-import) component maps columns and previews errors before anything lands, and our guide to a [React CSV importer](https://wingo-ui.com/blog/react-csv-importer) covers that flow.

## How do you validate cells and save them to a server?

Validation lives on the column. `required`, `min`, `max`, `step` and `options` cover the common rules, and `validate(value, row)` returns a message for anything else. A bad value stays in the cell with a red corner and the message under the active cell, and the toolbar shows an error count that jumps to the next one. Keeping the wrong value matters: after a 200-row paste, people want to fix the three bad cells, not retype them.

Saving follows one contract. `onCellChange` fires for every committed cell, `onCellsChange` for batches (a paste, a cleared range, undo and redo). Return `false` or a rejected promise and the grid puts the old values back:

```tsx
<EditableGrid
  columns={columns}
  value={rows}
  onValueChange={setRows}
  onCellChange={async ({ rowId, columnId, value }) => {
    const res = await fetch(`/api/products/${rowId}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ [columnId]: value }),
    })
    if (!res.ok) throw new Error("Save failed")
  }}
  onCellsChange={async (changes) => {
    const res = await fetch("/api/products/batch", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(changes),
    })
    if (!res.ok) throw new Error("Save failed")
  }}
/>
```

Two details matter on the server side. A save that fails also drops that step from the undo history, so Ctrl+Z never replays a change the server refused. And the cell callbacks only carry cell values: rows that are added or deleted, including the rows a tall paste appends, reach you through `onValueChange`. So either let the batch endpoint upsert unknown ids, or sync added and deleted rows from the rows you get in `onValueChange`.

If the rows only make sense together, like an import or an order with line items, skip the per-cell saves: keep the grid controlled and send `rows` with one Save button once the error count is zero. For the order case specifically there is an [Invoice Line Items](https://wingo-ui.com/components/invoice-line-items) component with totals and tax built in.

## How do undo and redo work?

Ctrl+Z or Cmd+Z undoes, Shift+Ctrl+Z or Ctrl+Y redoes, and each undo or redo goes through `onCellsChange` like any other batch, so the server stays in sync. A paste or a cleared range is one step, not one per cell, and deleted rows come back in their original place. The grid keeps the last 200 steps. You can also drive it from your own buttons: `setApi` hands you `undo()`, `redo()`, `addRow()`, `deleteRows(ids)` and `focusCell(ref)`, which is how the bulk delete example on the docs page offers Undo in a toast.

## When is inline edit in a data table enough?

When the table is mostly for reading. A CRM contact list, an orders page or a user admin table needs search, filters, sorting and row actions first, and editing second. Turning that into a spreadsheet makes reading worse. Put an Inline Edit in the cells that change often and leave the rest as text:

```tsx
import { DataTable, createColumns } from "@/components/ui/data-table"
import { InlineEdit } from "@/components/ui/inline-edit"

type Order = { id: string; customer: string; status: string; amount: number }

const STATUSES = [
  { value: "open", label: "Open" },
  { value: "paid", label: "Paid" },
  { value: "refunded", label: "Refunded" },
]

async function saveOrder(id: string, patch: Partial<Order>) {
  const res = await fetch(`/api/orders/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(patch),
  })
  if (!res.ok) throw new Error("Could not save the order")
}

const col = createColumns<Order>()
const columns = col([
  col.accessor("customer", { header: "Customer", filter: "text" }),
  col.accessor("status", {
    header: "Status",
    filter: "multi-select",
    cell: ({ row }) => (
      <InlineEdit
        type="select"
        size="sm"
        aria-label={`Status of ${row.customer}'s order`}
        options={STATUSES}
        defaultValue={row.status}
        onSave={(status) => saveOrder(row.id, { status })}
      />
    ),
  }),
  col.accessor("amount", {
    header: "Amount",
    align: "end",
    cell: ({ row }) => (
      <InlineEdit
        type="number"
        size="sm"
        aria-label={`Amount of ${row.customer}'s order`}
        defaultValue={row.amount}
        locale="en-US"
        format={{ style: "currency", currency: "USD" }}
        validate={(v) => (v == null || v < 0 ? "Enter an amount of 0 or more" : null)}
        onSave={(amount) => saveOrder(row.id, { amount: amount ?? 0 })}
      />
    ),
  }),
])

export function Orders({ orders }: { orders: Order[] }) {
  return <DataTable data={orders} columns={columns} caption="Orders" locale="en-US" />
}
```

Inline Edit saves optimistically: the new value shows at once with a small spinner, and if `onSave` rejects it rolls back, shakes and shows the error. Enter saves, Escape cancels, Tab or a click elsewhere saves. The Data Table ignores clicks on buttons and inputs inside a row, so editing a cell does not also trigger the row's click or selection.

> Live demo (Inline Edit): Click the listing title, change it and press Enter: the new value shows at once and a check pops in when the save lands. Try it at [Inline Edit](https://wingo-ui.com/components/inline-edit) and install it with `npx wingo-ui@latest add inline-edit`.

## How does an editable table work on a phone?

It works when the editing moves out of the cell and into a form. A grid of 40px cells is hard to hit with a thumb, and an editor inside a cell that is half off screen fights the on-screen keyboard. The Editable Grid keeps the spreadsheet on screen but changes the editing below 768px: the grid scrolls both ways with the first column frozen, one tap selects a cell, and a second tap (or a tap on the row number) opens the whole row as a form in a bottom sheet with a Save row button. A long press on the row number shows insert, duplicate and delete. Rows grow to 44px on touch screens.

The Data Table goes further and turns rows into cards below 768px, and an Inline Edit inside a card still works, with 44px save and cancel targets and its select opening as a bottom sheet. We wrote up the reasoning for cards in [responsive tables on mobile](https://wingo-ui.com/blog/responsive-table-mobile-cards).

## How does this compare with AG Grid and MUI?

Both are mature grids with far more features, and both put the spreadsheet parts behind a paid tier. Basic cell editing is free in both (AG Grid Community, the MUI X Data Grid). Facts as of November 5, 2026, from the vendors' docs:

| | Clipboard paste | Range (cell) selection | Source |
| --- | --- | --- | --- |
| AG Grid | Enterprise | Enterprise | [Clipboard](https://www.ag-grid.com/react-data-grid/clipboard/), [Cell selection](https://www.ag-grid.com/react-data-grid/cell-selection/), [Licensing](https://www.ag-grid.com/react-data-grid/licensing/) |
| MUI X Data Grid | Premium plan | Premium plan | [Clipboard](https://mui.com/x/react-data-grid/clipboard/), [Cell selection](https://mui.com/x/react-data-grid/cell-selection/) |
| Wingo UI Editable Grid | Included | Included | [Editable Grid docs](https://wingo-ui.com/components/editable-grid) |

Pick AG Grid or MUI when you need what our grid does not have: sorting and filtering inside the grid, formulas, a fill handle, row grouping, pivoting, Excel file export or server-side row models for millions of rows. Pick the Editable Grid when you want a React spreadsheet component as source code in your repo, styled with your Tailwind tokens, with a phone layout, and with the editing parts that most business screens actually use.

## Where should you start?

Open the [Editable Grid docs](https://wingo-ui.com/components/editable-grid): the playground plus examples for pasting from Excel, 2,000 rows, validation, bulk delete with undo, a timesheet and the phone row sheet. Editable Grid, Data Table and Inline Edit are part of Wingo UI Pro: $8 a month, $80 a year or $150 once as of November 2026 ([pricing](https://wingo-ui.com/pricing)). For the other inputs on an editing screen, read the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide), and find more in the [component guides category](https://wingo-ui.com/blog/category/components).

## Components in this post

- [Editable Grid](https://wingo-ui.com/components/editable-grid) (Pro): A React editable grid that works like a spreadsheet: typed cells edited in place, range selection, Excel copy and paste, undo, validation. Install: `npx wingo-ui@latest add editable-grid`
- [Data Table](https://wingo-ui.com/components/data-table) (Pro): A React data table on TanStack Table: search, filters, sorting, bulk actions, pinning, resizing, virtualization and cards on phones. Install: `npx wingo-ui@latest add data-table`
- [Inline Edit](https://wingo-ui.com/components/inline-edit) (Pro): A React inline edit field for text, numbers and selects that edits in place and saves optimistically. Install: `npx wingo-ui@latest add inline-edit`

## FAQ

### Does shadcn/ui have an editable table component?

No. The shadcn Data Table page is a guide to building your own table on TanStack Table, and it covers sorting, filtering, pagination, column visibility and row selection, but not cell editing. For editing you render your own inputs in the cell renderer or use a grid component.

### How do I paste data from Excel into a React table?

Listen for the paste event on the focused grid, read event.clipboardData.getData("text/plain"), and split it on newlines and tabs: Excel and Google Sheets put a copied range on the clipboard as tab separated values. Then convert each string to the column's type (numbers in the user's locale, dates, option labels to values) and write the block starting at the active cell.

### How do I make a single cell editable in a React table?

Render an inline edit control inside that column's cell renderer: it shows the value as text, turns into an input on click or Enter, and calls a save function on Enter or blur. Keep the row's id in the save call so the server knows which record changed, and roll back the value if the request fails.

### Should an editable table save every cell or the whole table at once?

Save per cell when each change stands on its own, like a price or a status, and roll back the one cell that fails. Save the whole sheet with one button when the rows only make sense together, like an import or an order with line items, and validate everything before the request.

### Is AG Grid free for copy and paste and range selection?

As of November 2026, AG Grid's documentation marks clipboard operations and cell (range) selection as Enterprise features, which need a paid license in production. Basic cell editing is available in the free Community edition.

---

Source: https://wingo-ui.com/blog/react-editable-table
