Shadcn React Table

Search documentation

Search the docs

Editing

Set enableEditing: true to make rows editable, then choose how editing is presented with editDisplayMode. Per-column behavior is configured through column meta.

Cell editing

editDisplayMode: "cell" is the default. Click a cell to edit it inline and commit the new value with onEditingCellSave.

Editing — cellClick a cell to edit it inline.
GitHub
ID
Jan 2, 2023
0%
Jan 13, 2023
9%
Jan 24, 2023
18%
Feb 4, 2023
27%
Feb 15, 2023
36%
Feb 26, 2023
45%
Mar 9, 2023
54%
Mar 20, 2023
63%
Mar 31, 2023
72%
Apr 11, 2023
81%
Apr 22, 2023
90%
May 3, 2023
99%
$681,042
tsx
const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  enableEditing: true,
  editDisplayMode: "cell",
  onEditingCellSave: ({ row, column, value }) => {
    setData((prev) =>
      prev.map((r) => (r.id === row.id ? { ...r, [column.id]: value } : r))
    )
  },
})

return <DataTable table={table} />

Row editing

editDisplayMode: "row" edits a whole row at once with save/cancel controls. Persist the row with onEditingRowSave, then call exit() to leave edit mode.

Editing — rowEdit a whole row inline with save/cancel.
GitHub
ID
Row actions
AvaThompsonOwnerEngineeringactive22$45,000Jan 2, 2023
0%
LiamNguyenAdminMarketinginactive25$47,137Jan 13, 2023
9%
NoahSilvaEditorDesignpending28$49,274Jan 24, 2023
18%
EmmaCarterViewerSalesactive31$51,411Feb 4, 2023
27%
OliviaRossiOwnerSupportinactive34$53,548Feb 15, 2023
36%
WilliamWalkerAdminEngineeringpending37$55,685Feb 26, 2023
45%
SophiaPatelEditorMarketingactive40$57,822Mar 9, 2023
54%
JamesMullerViewerDesigninactive43$59,959Mar 20, 2023
63%
IsabellaParkOwnerSalespending46$62,096Mar 31, 2023
72%
LucasReyesAdminSupportactive49$64,233Apr 11, 2023
81%
MiaJohanssonEditorEngineeringinactive52$66,370Apr 22, 2023
90%
BenjaminCostaViewerMarketingpending55$68,507May 3, 2023
99%
$681,042
tsx
const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  enableEditing: true,
  editDisplayMode: "row",
  onEditingRowSave: ({ row, values, exit }) => {
    setData((prev) =>
      prev.map((r) => (r.id === row.id ? { ...r, ...values } : r))
    )
    exit()
  },
})

return <DataTable table={table} />

Table editing

editDisplayMode: "table" keeps every editable cell in edit mode at all times, like a spreadsheet.

Editing — tableEvery editable cell is always editable.
GitHub
ID
Jan 2, 2023
0%
Jan 13, 2023
9%
Jan 24, 2023
18%
Feb 4, 2023
27%
Feb 15, 2023
36%
Feb 26, 2023
45%
Mar 9, 2023
54%
Mar 20, 2023
63%
Mar 31, 2023
72%
Apr 11, 2023
81%
Apr 22, 2023
90%
May 3, 2023
99%
$681,042
tsx
const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  enableEditing: true,
  editDisplayMode: "table",
  onEditingCellSave: ({ row, column, value }) => {
    setData((prev) =>
      prev.map((r) => (r.id === row.id ? { ...r, [column.id]: value } : r))
    )
  },
})

editDisplayMode: "modal" edits (and creates) rows in a dialog. Use onEditingRowSave for edits and onCreatingRowSave for new rows; both receive exit() to close the dialog. Seed the create form with createRowDefaults, and open it from the toolbar via table.beginCreate().

Editing — modalEdit or create a row in a dialog.
GitHub
ID
Row actions
AvaThompsonOwnerEngineeringactive22$45,000Jan 2, 2023
0%
LiamNguyenAdminMarketinginactive25$47,137Jan 13, 2023
9%
NoahSilvaEditorDesignpending28$49,274Jan 24, 2023
18%
EmmaCarterViewerSalesactive31$51,411Feb 4, 2023
27%
OliviaRossiOwnerSupportinactive34$53,548Feb 15, 2023
36%
WilliamWalkerAdminEngineeringpending37$55,685Feb 26, 2023
45%
SophiaPatelEditorMarketingactive40$57,822Mar 9, 2023
54%
JamesMullerViewerDesigninactive43$59,959Mar 20, 2023
63%
IsabellaParkOwnerSalespending46$62,096Mar 31, 2023
72%
LucasReyesAdminSupportactive49$64,233Apr 11, 2023
81%
MiaJohanssonEditorEngineeringinactive52$66,370Apr 22, 2023
90%
BenjaminCostaViewerMarketingpending55$68,507May 3, 2023
99%
$681,042
tsx
const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  enableEditing: true,
  editDisplayMode: "modal",
  createRowDefaults: { name: "", role: "member" },
  onEditingRowSave: ({ row, values, exit }) => {
    setData((prev) =>
      prev.map((r) => (r.id === row.id ? { ...r, ...values } : r))
    )
    exit()
  },
  onCreatingRowSave: ({ values, exit }) => {
    setData((prev) => [...prev, { id: crypto.randomUUID(), ...values }])
    exit()
  },
  renderTopToolbarCustomActions: ({ table }) => (
    <Button size="sm" onClick={() => table.beginCreate()}>
      New row
    </Button>
  ),
})

Cancelling

onEditingRowCancel({ row, table }) and onCreatingRowCancel({ table }) run when the user cancels — the Cancel button, closing the dialog, or the row's cancel button. They don't run after a save calls exit().

Custom dialog content

renderEditRowDialogContent and renderCreateRowDialogContent replace what's inside the dialog. They receive the built-in fields as internalEditComponents, plus save(), cancel() and hasErrors. Include a DialogTitle so the dialog keeps its accessible name.

tsx
import { DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"

useDataTable({
  data,
  columns,
  enableEditing: true,
  editDisplayMode: "modal",
  renderEditRowDialogContent: ({ row, internalEditComponents, save, cancel, hasErrors }) => (
    <>
      <DialogHeader>
        <DialogTitle>Edit {row.original.name}</DialogTitle>
      </DialogHeader>
      <div className="grid grid-cols-2 gap-3">{internalEditComponents}</div>
      <DialogFooter>
        <Button variant="outline" onClick={cancel}>Discard</Button>
        <Button onClick={save} disabled={hasErrors}>Save changes</Button>
      </DialogFooter>
    </>
  ),
})
Custom edit dialogrenderEditRowDialogContent: a two-column form.
GitHub
ID
Row actions
AvaThompsonOwnerEngineeringactive22$45,000Jan 2, 2023
0%
LiamNguyenAdminMarketinginactive25$47,137Jan 13, 2023
9%
NoahSilvaEditorDesignpending28$49,274Jan 24, 2023
18%
EmmaCarterViewerSalesactive31$51,411Feb 4, 2023
27%
OliviaRossiOwnerSupportinactive34$53,548Feb 15, 2023
36%
WilliamWalkerAdminEngineeringpending37$55,685Feb 26, 2023
45%
SophiaPatelEditorMarketingactive40$57,822Mar 9, 2023
54%
JamesMullerViewerDesigninactive43$59,959Mar 20, 2023
63%
IsabellaParkOwnerSalespending46$62,096Mar 31, 2023
72%
LucasReyesAdminSupportactive49$64,233Apr 11, 2023
81%
$681,042
Rows per page
1–10 of 12

Inline create row position

With createDisplayMode: "row", the create row sits above the data. Set positionCreatingRow: "bottom" to put it below, or a row index (positionCreatingRow: 3) to insert it before that row on the current page.

Inline create rowAn inline create row at the bottom of the table.
GitHub
ID
Jan 2, 2023
0%
Jan 13, 2023
9%
Jan 24, 2023
18%
Feb 4, 2023
27%
Feb 15, 2023
36%
Feb 26, 2023
45%
Mar 9, 2023
54%
Mar 20, 2023
63%
$419,836
Rows per page
1–8 of 8

Per-column editing config

Configure editors on each column's meta:

  • editVariant: "text" | "number" | "select" — the input type.
  • editSelectOptions — options for the "select" variant, or ({ row, column, table }) => options to vary them by row (row is undefined when creating).
  • validate: (value) => string | undefined — return a message to block the save.
  • enableEditing: false — lock a single column, or (row) => boolean to lock it on some rows.
tsx
const columns = [
  {
    accessorKey: "role",
    header: "Role",
    meta: {
      editVariant: "select",
      editSelectOptions: ["admin", "member", "viewer"],
    },
  },
  {
    accessorKey: "email",
    header: "Email",
    meta: {
      editVariant: "text",
      validate: (value) =>
        /.+@.+\..+/.test(value) ? undefined : "Enter a valid email",
    },
  },
  {
    accessorKey: "id",
    header: "ID",
    meta: { enableEditing: false },
  },
]

Editing some rows only

Pass a function as enableEditing to decide row by row. Rows it returns false for show no click-to-edit cells and no edit button; creating new rows still works. The same works per column with meta.enableEditing.

tsx
const table = useDataTable({
  data,
  columns: [
    { accessorKey: "name", header: "Name" },
    {
      accessorKey: "salary",
      header: "Salary",
      // Only managers' salaries are editable.
      meta: { enableEditing: (row) => row.original.role === "manager" },
    },
  ],
  enableEditing: (row) => !row.original.archived,
})

To check a row yourself, call table.dataTable.getCanEditRow(row).

Editing some rows onlyenableEditing as a function: inactive people are locked.
GitHub
ID
Jan 2, 2023
0%
LiamNguyenAdminMarketinginactive25$47,137Jan 13, 2023
9%
Jan 24, 2023
18%
Feb 4, 2023
27%
OliviaRossiOwnerSupportinactive34$53,548Feb 15, 2023
36%
Feb 26, 2023
45%
Mar 9, 2023
54%
JamesMullerViewerDesigninactive43$59,959Mar 20, 2023
63%
Mar 31, 2023
72%
Apr 11, 2023
81%
$681,042
Rows per page
1–10 of 12

Custom editor

When editVariant isn't enough, supply your own editor with meta.renderEditCell. It replaces the built-in field while the cell/row is editing; drive the value through the table instance (setRowDraftValue for row/modal editing, or onEditingCellSave for cell editing):

tsx
{
  accessorKey: "status",
  header: "Status",
  meta: {
    renderEditCell: ({ cell, table }) => (
      <MyStatusPicker
        value={cell.getValue() as string}
        onChange={(value) =>
          table.setRowDraftValue(cell.column.id, value)
        }
      />
    ),
  },
}

Relevant options

useDataTable options that drive editing and creating:

PropTypeDefaultDescription
enableEditing?boolean | ((row: DataTableRow<TData>) => boolean)falseEnable inline editing, or decide per row with a function (rows it returns false for can't be edited; creating rows is still allowed).
editDisplayMode?EditDisplayMode"cell"How edits are surfaced. Default "cell".
createDisplayMode?CreateDisplayMode"modal"How the create form is surfaced, independent of `editDisplayMode`: `"modal"` (dialog, default), `"row"` (an inline editable row at the top of the table), or `"custom"` (you render it from `isCreating` + `rowDraft`).
createRowDefaults?Record<string, unknown>—Default values for the create form, keyed by column id.
onEditingCellSave?(props: { row: DataTableRow<TData> column: DataTableColumn<TData, unknown> value: unknown table: DataTableInstance<TData> }) => void—Called when a cell edit is committed (cell and table editing) with the new `value`. Persist it; the table does not change `data` itself.
onEditingRowSave?(props: { row: DataTableRow<TData> values: Record<string, unknown> table: DataTableInstance<TData> exit: () => void }) => void—Called when a row edit is saved (row and modal editing) with the edited `values`. Call `exit()` to leave editing, e.g. after the save succeeds.
onCreatingRowSave?(props: { values: Record<string, unknown> table: DataTableInstance<TData> exit: () => void }) => void—Called when the new-row editor is saved with the row's `values`. Call `exit()` to close it.

Per-column, via columnDef.meta:

meta keyTypeDescription
enableEditing?boolean | ((row: DataTableRow<TData>) => boolean)Allow editing this column (defaults to true when table editing is on), or decide per row with a function.
editVariant?EditVariantInline editor variant. Defaults to "text".
editSelectOptions?| DataTableFilterOption[] | ((props: { row?: DataTableRow<TData> column: DataTableColumn<TData, unknown> table: DataTableInstance<TData> }) => DataTableFilterOption[])Options for the "select" edit variant, or a function returning them for the row being edited (`row` is undefined in the create form).
renderEditCell?(props: CellRenderProps<TData, TValue>) => React.ReactNodeCustom inline editor for this column (escape hatch). Replaces the built-in editor while the cell/row is editing; read the draft from `table.getState().rowDraft` and write it with `table.setRowDraftValue` (row editing), or save with `onEditingCellSave` (cell editing).
validate?(value: unknown) => string | undefinedValidate an edited value; return an error message or undefined if valid.