Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
  1. Home
  2. Blog
  3. Component guides
  4. React Editable Table: Spreadsheet Grid or Inline Edit
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Editable Table: Spreadsheet Grid or Inline Edit
Component guides
Component guides

React Editable Table: Spreadsheet Grid or Inline Edit

SR
Serban Rusu · Founder of Wingo UI
Nov 5, 2026 · 9 min read

On this page

0%
  1. Which kind of React editable table do you need?
  2. Does shadcn have an editable table?
  3. How do you build a spreadsheet-like editable grid in React?
  4. How does Excel copy and paste work in a React grid?
  5. How do you validate cells and save them to a server?
  6. How do undo and redo work?
  7. When is inline edit in a data table enough?
  8. How does an editable table work on a phone?
  9. How does this compare with AG Grid and MUI?
  10. Where should you start?
  11. FAQ
    1. Does shadcn/ui have an editable table component?
    2. How do I paste data from Excel into a React table?
    3. How do I make a single cell editable in a React table?
    4. Should an editable table save every cell or the whole table at once?
    5. Is AG Grid free for copy and paste and range selection?

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.

Published Nov 5, 2026

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 for the spreadsheet case and Inline Edit inside the 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 tableEditable grid
Main jobBrowse, search, filter, fix one valueEnter and correct many rows
Edit modelClick a value, edit, save that valueType over any cell, Enter and Tab move like Excel
SelectionRows (checkboxes, bulk actions)Cells and ranges, rows, columns
ClipboardCopy text like any pageCopy, cut and paste blocks as tab separated values
UndoPer field (Escape cancels)Undo and redo across edits, pastes and deletes
SavingOne request per value, rollback on failurePer cell, per batch or the whole sheet
ExamplesCRM contacts, orders, user adminPrice lists, timesheets, budgets, imports

Does shadcn have an editable table?

It does not. The shadcn Data Table page (opens in a new tab) (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 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.

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
$ npx wingo-ui@latest add editable-grid
ProEditable Grid docs

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 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 component maps columns and previews errors before anything lands, and our guide to a 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 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.

Click the listing title, change it and press Enter: the new value shows at once and a check pops in when the save lands
$ npx wingo-ui@latest add inline-edit
ProInline Edit docs

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.

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 pasteRange (cell) selectionSource
AG GridEnterpriseEnterpriseClipboard (opens in a new tab), Cell selection (opens in a new tab), Licensing (opens in a new tab)
MUI X Data GridPremium planPremium planClipboard (opens in a new tab), Cell selection (opens in a new tab)
Wingo UI Editable GridIncludedIncludedEditable Grid docs

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: 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). For the other inputs on an editing screen, read the React form components guide, and find more in the component guides category.

Components in this post

  • Editable Grid

    A React editable grid that works like a spreadsheet: typed cells edited in place, range selection, Excel copy and paste, undo, validation.

    Pro
  • Data Table

    A React data table on TanStack Table: search, filters, sorting, bulk actions, pinning, resizing, virtualization and cards on phones.

    Pro
  • Inline Edit

    A React inline edit field for text, numbers and selects that edits in place and saves optimistically.

    Pro

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.

Try it in your project

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.

75 components free, no account needed. Pro from $8 a month.

Open Editable Grid liveSee pricing
  • Editable table
  • Data grid
  • Spreadsheet
  • Inline edit
  • React

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Component guides

Audit Log UI in React: Filters, Diffs and CSV Export

Build an audit log UI in React that people can use: who did what to which record, day groups, filters, before and after diffs, CSV export and the event shape.

SRSerban Rusu·Nov 6, 2026·10 min read
Component guides

React Filter Builder: AND / OR Chips and Saved Views

Build a React filter builder for any list: condition chips, AND / OR groups, saved views, a live result count, a phone bottom sheet and a server-side query.

SRSerban Rusu·Nov 6, 2026·11 min read
Component guides

React Empty State Component: First Use, No Results, Errors

Build a React empty state component for first use, no results and errors, with copy that says what happened and one action that moves people forward.

SRSerban Rusu·Oct 19, 2026·8 min read
Newsletter

Get new posts by email

New posts, product news and occasional offers from Wingo UI. We count clicks to keep it relevant.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • For teams
  • Theme generator

Resources

  • Docs
  • Contact
  • Programs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Compare

  • Wingo UI vs paid kits
  • Wingo UI vs shadcn/ui
  • shadcn/ui alternatives
  • MUI alternatives
  • Tailwind Plus alternatives
  • Magic UI alternatives
  • Free components

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI