Inline editing
Click an editable cell to swap its content for an inline editor. Commit with Enteror by clicking away; Esc discards the change. The library doesn't own your data . You handle the update in onCellEdit.
Six editor types are built in: text, number, date,checkbox, select, and custom. The cell itself becomes the editor: full-bleed input, primary-color focus ring, no nested borders. Add avalidate function on any column to reject invalid input or surface an inline error message.
New in 8.1.0 — number, date, checkbox, and custom editors plus thevalidate hook. The text and select editors are unchanged.
Editable cells — text + dropdown
Name and Salary use text inputs with validation. Department and Status use dropdowns. Click any cell to edit.
Click any cell to edit. Name and Salary are text inputs; Department and Status are dropdowns; Remote is a checkbox. Enter commits, Esc cancels. Name and Salary are validated: try committing an empty name or a negative salary to see the inline error. Keyboard navigation is enabled too: click or Tab into the table, move between cells and headers with the arrow keys, and press Enter to edit or sort.
import { useState } from 'react';
import DataTable, { type TableColumn } from 'react-data-table-component';
interface Employee {
id: number;
name: string;
department: 'Engineering' | 'Product' | 'Design';
status: 'Active' | 'On Leave' | 'Terminated';
salary: number;
}
export default function App() {
const [data, setData] = useState<Employee[]>(initialData);
const handleCellEdit = (row: Employee, value: string, column: TableColumn<Employee>) => {
const field = column.id as keyof Employee;
setData(prev =>
prev.map(r => (r.id === row.id ? { ...r, [field]: field === 'salary' ? Number(value) : value } : r)),
);
};
const columns: TableColumn<Employee>[] = [
// Text editor — editable: true is shorthand for { editor: { type: 'text' } }
{
id: 'name',
name: 'Name',
selector: r => r.name,
editable: true,
validate: value => (value.trim() === '' ? 'Name is required' : true),
onCellEdit: handleCellEdit,
},
// Dropdown editor
{
id: 'department',
name: 'Department',
selector: r => r.department,
editor: {
type: 'select',
options: [
{ value: 'Engineering', label: 'Engineering' },
{ value: 'Product', label: 'Product' },
{ value: 'Design', label: 'Design' },
],
},
onCellEdit: handleCellEdit,
},
// Custom cell renderer + dropdown editor compose freely
{
id: 'status',
name: 'Status',
selector: r => r.status,
cell: row => <StatusBadge status={row.status} />,
editor: {
type: 'select',
options: [
{ value: 'Active', label: 'Active' },
{ value: 'On Leave', label: 'On Leave' },
{ value: 'Terminated', label: 'Terminated' },
],
},
onCellEdit: handleCellEdit,
},
{
id: 'salary',
name: 'Salary',
selector: r => r.salary,
format: r => `$${r.salary.toLocaleString()}`,
right: true,
editable: true,
validate: value => {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? true : 'Enter a positive number';
},
onCellEdit: handleCellEdit,
},
];
return <DataTable columns={columns} data={data} />;
}How it works
- Click an editable cell. The cell content is replaced by the editor, seeded with the current selector value (stringified).
- Press Enter or click outside.
onCellEdit(row, value, column)fires. - Press Esc. The editor closes without firing the callback.
The callback always receives value as a string. Cast or parse it to the type you actually store (number, boolean, date, etc.) in your update handler.
Editor types
Pick the right editor for the value type. Every editor calls onCellEdit(row, value, column)with the new value as a string — you parse it into a number, boolean, or date in your update handler.
Text input
The default. editable: true is shorthand foreditor: { type: 'text' }:
// Shorthand
{ id: 'name', name: 'Name', selector: r => r.name,
editable: true, onCellEdit }
// Equivalent explicit form — supports placeholder
{ id: 'name', name: 'Name', selector: r => r.name,
editor: { type: 'text', placeholder: 'Enter a name…' },
onCellEdit }Number input
A native type="number" field with optional min, max, and step:
{
id: 'salary', name: 'Salary', selector: r => r.salary,
editor: { type: 'number', min: 0, step: 1000 },
onCellEdit, // value arrives as a string — Number(value) it on commit
}Date picker
A native type="date" field. Values arrive in ISO YYYY-MM-DD form:
{
id: 'joined', name: 'Joined', selector: r => r.joined,
editor: { type: 'date', min: '2020-01-01' },
onCellEdit,
}Checkbox
A toggle that commits immediately on click. The selector should return a boolean (or a string 'true' / 'false'). The callback receives thenew string value:
{
id: 'active', name: 'Active', selector: r => r.active,
editor: { type: 'checkbox' },
onCellEdit: (row, value) => {
setData(prev => prev.map(r => r.id === row.id ? { ...r, active: value === 'true' } : r));
},
}Dropdown (select)
Provide a list of { value, label } options. The select commits the selected value immediately on change and on blur, so users don't need to pressEnter:
{
id: 'status', name: 'Status', selector: r => r.status,
editor: {
type: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'on_leave', label: 'On Leave' },
{ value: 'terminated', label: 'Terminated' },
],
placeholder: 'Choose status…', // optional, shown when value is empty
},
onCellEdit,
}value is the string the callback receives. label is what the user sees in the dropdown. It can be any string or number (rich React content isn't supported inside <option> elements).
Custom editor
Render any editor you want and call ctx.commit(value) when ready orctx.cancel() to discard. The context also exposes value /setValue so you can wire up controlled inputs:
{
id: 'role', name: 'Role', selector: r => r.role,
editor: {
type: 'custom',
render: ({ value, setValue, commit, cancel, row, inputRef }) => (
<Autocomplete
ref={inputRef}
value={value}
onChange={setValue}
onSelect={v => commit(v)}
onBlur={() => commit()}
onEscape={cancel}
/>
),
},
onCellEdit,
}The ctx.row field gives you the row data for context-aware editors (dependent dropdowns, etc.). The cell wrapper still owns padding and the focus ring — your custom editor only needs to render the input.
Validation works the same as for built-in editors: ctx.commit runs the column's validate first, and a string result keeps the editor open with the inline error tooltip. Since 8.7.0 the render context also carries the current error asctx.error (string | null), so your editor can style its own invalid state, e.g. aria-invalid={!!ctx.error}. Attachctx.inputRef as the ref of your focusable element to get the same focus handling as the built-in editors: auto-focus when the editor opens and refocus after a rejected commit.
The cell becomes the editor
When editing starts, the cell drops its padding, paints a soft tinted background, and shows a 2 px primary-color focus ring around its full footprint. The text input or select fills the entire cell. No floating boxes, no nested borders. Both editing and idle hover states use CSS custom properties so they automatically follow your theme.
Customizable variables
| Variable | Default | Purpose |
|---|---|---|
--rdt-color-cell-edit-bg | 8% primary on bg | Background of the cell while editing. |
--rdt-color-cell-edit-hover | 6% primary | Background of an editable cell on hover. |
--rdt-color-cell-edit-hover-border | 40% primary | Color of the dashed underline that hints "this cell is editable" on hover. |
.rdt_table {
--rdt-color-cell-edit-bg: #fff7ed; /* warm amber */
--rdt-color-cell-edit-hover: #fffbeb;
--rdt-color-cell-edit-hover-border: #f59e0b;
}Updating row data
DataTable is fully controlled. Committing an edit does not mutate your data. You're responsible for producing a new array with the change applied. The most common pattern is React state plus an immutable update:
const handleCellEdit = (row, value, column) => {
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [column.id as string]: value } : r)));
};For server-backed data, fire your mutation here and either optimistically update the local state or wait for the server response:
const handleCellEdit = async (row, value, column) => {
// Optimistic update
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [column.id as string]: value } : r)));
try {
await api.updateEmployee(row.id, { [column.id as string]: value });
} catch (err) {
// Roll back on failure
setData(prev =>
prev.map(r => (r.id === row.id ? { ...r, [column.id as string]: row[column.id as keyof typeof row] } : r)),
);
console.error('Save failed:', err);
}
};Server-side saves — optimistic update with rollback
Edits apply immediately and save to a simulated server. Failed saves roll back. The server rejects prices over $1,000.
Edits apply optimistically, then save to a simulated server with ~1s latency. The row dims while the save is in flight. The server rejects any Price over $1,000 — try it to watch the edit roll back.
import { useState } from 'react';
import DataTable, { type TableColumn } from 'react-data-table-component';
export default function App() {
const [data, setData] = useState<Product[]>(initialData);
const [savingId, setSavingId] = useState<number | null>(null);
const handleCellEdit = async (row: Product, value: string, column: TableColumn<Product>) => {
const field = column.id as keyof Product;
const parsed = field === 'name' ? value : Number(value);
// Optimistic update
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [field]: parsed } : r)));
setSavingId(row.id);
try {
await api.updateProduct(row.id, { [field]: parsed });
} catch (err) {
// Roll back the field to its pre-edit value
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [field]: row[field] } : r)));
} finally {
setSavingId(null);
}
};
const columns: TableColumn<Product>[] = [
{ id: 'sku', name: 'SKU', selector: r => r.sku },
{ id: 'name', name: 'Product', selector: r => r.name, editable: true, onCellEdit: handleCellEdit },
{
id: 'price',
name: 'Price',
selector: r => r.price,
right: true,
editor: { type: 'number', min: 0 },
validate: value => (Number(value) > 0 ? true : 'Enter a positive number'),
onCellEdit: handleCellEdit,
},
];
return (
<DataTable
columns={columns}
data={data}
conditionalRowStyles={[{ when: r => r.id === savingId, style: { opacity: 0.45 } }]}
/>
);
}Validation
Add a validate function to any column to gate the edit beforeonCellEdit fires. Return:
true— accept the value, commit the edit.false— reject silently, close the editor without firing the callback.- A
string— keep the editor open, paint an error ring, and show the message as an inline tooltip.
{
id: 'salary', name: 'Salary', selector: r => r.salary,
editor: { type: 'number', min: 0 },
validate: (value) => {
const n = Number(value);
if (Number.isNaN(n)) return 'Must be a number';
if (n < 0) return 'Cannot be negative';
if (n > 1_000_000) return false; // silently reject
return true;
},
onCellEdit,
}The error state uses CSS variables you can theme:--rdt-color-cell-edit-error drives both the error ring and the tooltip background.
Restricting which columns are editable
Only set editable: true on the columns you want users to be able to change. Columns without it remain read-only. You can also conditionally toggle the flag based on user permissions:
const canEdit = user.role === 'admin';
const columns: TableColumn<Employee>[] = [
{ id: 'name', name: 'Name', selector: r => r.name }, // never editable
{ id: 'salary', name: 'Salary', selector: r => r.salary, editable: canEdit, onCellEdit: handleCellEdit }, // editable only for admins
];Combining with custom cell renderers
A column can have both a custom cell renderer and aneditor. The cell renderer is used for display; clicking the cell switches to the inline editor. This is the pattern shown in the demo above. Status renders as a coloured badge but edits as a dropdown.
{
id: 'status', name: 'Status', selector: r => r.status,
cell: row => <StatusBadge status={row.status} />,
editor: {
type: 'select',
options: [
{ value: 'Active', label: 'Active' },
{ value: 'On Leave', label: 'On Leave' },
{ value: 'Terminated', label: 'Terminated' },
],
},
onCellEdit,
}If you want a fully custom editor (date picker, autocomplete, multi-select), skipeditor and let your cell renderer own both display and editing. Toggle an "editing" state inside your component:
function StatusEditableCell({ row, onChange }) {
const [editing, setEditing] = useState(false);
if (!editing) return (
<span onClick={() => setEditing(true)}>{row.status}</span>
);
return (
<CustomDatePicker
value={row.status}
onChange={v => { onChange(v); setEditing(false); }}
onBlur={() => setEditing(false)}
autoFocus
/>
);
}
{ id: 'status', cell: row => <StatusEditableCell row={row} onChange={v => update(row.id, v)} /> }Styling the input
Editor controls have class rdt_editInput (text) and rdt_editSelect(dropdown). Both inherit font and color from the cell and stretch to fill its full footprint. The cell itself uses rdt_cellEditable (idle hover state) andrdt_cellEditing (active edit state). Override any of them in your stylesheet:
/* Make the input bigger and chunkier */
.rdt_editInput,
.rdt_editSelect {
font-size: 14px;
font-weight: 500;
}
/* Use a thicker focus ring */
.rdt_cellEditing {
box-shadow: inset 0 0 0 3px var(--rdt-color-primary);
}Keyboard cell navigation
Pass cellNavigation to navigate the whole table with the keyboard, spreadsheet-style — arrow keys between cells, Enter/F2 to open the focused cell's editor, andEscape to cancel and return focus to the cell. SeeKeyboard navigation for the full key reference; it also covers header, selection, and expander cells, and works on read-only tables.
<DataTable columns={columns} data={data} cellNavigation />;Limitations
- Built-in editors cover text, number, date, checkbox, and select. For autocomplete, multi-line text, rich pickers, etc. use the
customeditor. - The
valuepassed toonCellEditandvalidateis always a string. Parse it yourself. - Dropdown
labels must be strings or numbers. They render inside native<option>elements which can't contain arbitrary React content. - There is no built-in "dirty" indicator or undo stack. Track that in your application state if you need it.
- Editing does not trigger row selection or row click handlers. Clicks inside the editor are stopped from propagating.
CellEditor
type CellEditor<T = unknown> =
| { type: 'text'; placeholder?: string }
| { type: 'number'; placeholder?: string; min?: number; max?: number; step?: number }
| { type: 'date'; min?: string; max?: string }
| { type: 'checkbox' }
| {
type: 'select';
options: Array<{ value: string; label: React.ReactNode }>;
placeholder?: string;
}
| {
type: 'custom';
render: (ctx: CustomCellEditorContext<T>) => React.ReactNode;
};
interface CustomCellEditorContext<T> {
row: T;
value: string;
setValue: (next: string) => void;
commit: (value?: string) => void;
cancel: () => void;
column: TableColumn<T>;
}See it combined with other features in the Editable inventory grid recipe.
Prop reference
| Column prop | Type | Description |
|---|---|---|
editable | boolean | Shorthand for a text editor. Equivalent to editor: { type: 'text' }. Ignored when editor is set. |
editor | CellEditor | Editor configuration. See CellEditor above for the full union. |
validate | (value, row, column) => true | false | string | Runs before onCellEdit. Return true to accept, false to reject silently, or a string to keep the editor open with an inline error. |
onCellEdit | (row: T, value: string, column: TableColumn<T>) => void | Called when the user commits an edit (Enter, blur, or selection change for dropdowns). Receives the original row, the new string value, and the column definition. |
cellNavigation is a table-level prop covered on its own page — seeKeyboard navigation.