Inline editing
Click an editable cell to swap its content for an inline editor. Commit with Enter
or by clicking away; Esc discards the change. The library doesn't own your data
. You handle the update in onCellEdit.
Two editor types are built in: a text input (editable: true as shorthand) and
a dropdown (editor: { type: 'select', options }). The cell itself
becomes the editor: full-bleed input, primary-color focus ring, no nested borders.
Editable cells — text + dropdown
Name and Salary use text inputs. 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. Enter commits, Esc cancels.
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) || r.salary : 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, 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, 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
Text input
The default. editable: true is shorthand for
editor: { 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 } 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 press
Enter:
{
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).
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);
}
}; Validation
Reject invalid input by checking inside onCellEdit and skipping the update.
The cell input doesn't expose a "reject and re-open" hook, so the simplest pattern is to
no-op and rely on the user re-clicking the cell.
const handleCellEdit = (row, value, column) => {
if (column.id === 'salary') {
const n = Number(value);
if (isNaN(n) || n < 0) return; // ignore invalid input
setData(prev => prev.map(r => r.id === row.id ? { ...r, salary: n } : r));
return;
}
setData(prev => prev.map(r => r.id === row.id ? { ...r, [column.id]: value } : r));
}; 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 an
editor. 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), skip
editor 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) and
rdt_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);
} Limitations
- Built-in editors are text and select only. For multi-line text, date pickers, autocompletes, or rich validation, render your own
cell(see above). - The
valuepassed toonCellEditis 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.
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. Either { type: 'text', placeholder? } or { type: 'select', options, placeholder? }. |
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. |
CellEditor
type CellEditor =
| { type: 'text'; placeholder?: string }
| {
type: 'select';
options: Array<{ value: string; label: React.ReactNode }>;
placeholder?: string;
};