Inputs
Inline editPro
Edit text, numbers, choices and dates where they are shown, with async saving, validation errors and a saved confirmation, without opening a form.
npx prodular-ui add inline-editPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Record details
Click a value to edit. Enter or leaving the field saves, Escape cancels. Try renaming the customer to Kestrel Health to see a server error.
- Customer
- Seats
- Account owner
- Renews
- Notes
"use client"
import * as React from "react"
import { InlineEdit } from "@/components/ui/inline-edit"
// Stands in for a PATCH request. Saves take 600ms.const wait = () => new Promise((r) => setTimeout(r, 600))
const owners = [ { value: "maya", label: "Maya Okafor" }, { value: "priya", label: "Priya Raman" }, { value: "theo", label: "Theo Lindqvist" },]
export default function InlineEditRecord() { const [name, setName] = React.useState("Northwind Logistics") const [seats, setSeats] = React.useState<number | null>(42) const [owner, setOwner] = React.useState<string | null>("priya") const [renewal, setRenewal] = React.useState<Date | null>(new Date(2027, 2, 31)) const [notes, setNotes] = React.useState("")
const rows: [string, React.ReactNode][] = [ [ "Customer", <InlineEdit key="name" label="Customer name" value={name} validate={(v) => (v ? null : "A customer needs a name.")} onSave={async (v) => { await wait() if (v.toLowerCase() === "kestrel health") return "A customer named Kestrel Health already exists." setName(v) }} />, ], [ "Seats", <InlineEdit key="seats" type="number" label="Seats" value={seats} min={1} max={500} unit="seats" onSave={async (v) => { await wait() setSeats(v) }} />, ], [ "Account owner", <InlineEdit key="owner" type="select" label="Account owner" value={owner} options={owners} placeholder="Assign an owner" onSave={async (v) => { await wait() setOwner(v) }} />, ], [ "Renews", <InlineEdit key="renewal" type="date" label="Renewal date" value={renewal} locale="en-US" disabledDates={{ before: new Date(2026, 9, 21) }} onSave={async (v) => { await wait() setRenewal(v) }} />, ], [ "Notes", <InlineEdit key="notes" label="Notes" value={notes} placeholder="Add a note for the team" onSave={async (v) => { await wait() setNotes(v) }} />, ], ]
return ( <dl className="w-full max-w-md divide-y rounded-xl border bg-card px-4 shadow-xs"> {rows.map(([term, field]) => ( <div key={term} className="grid grid-cols-[120px_1fr] items-center gap-4 py-2"> <dt className="text-[13px] text-muted-foreground">{term}</dt> <dd className="m-0 min-w-0">{field}</dd> </div> ))} </dl> )}In a table
Status and due date change in place. Pickers save as soon as a choice is made.
| Invoice | Customer | Status | Due |
|---|---|---|---|
| INV-2104 | Northwind Logistics | ||
| INV-2103 | Kestrel Health | ||
| INV-2101 | Meridian Freight |
"use client"
import * as React from "react"
import { InlineEdit } from "@/components/ui/inline-edit"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
type Invoice = { id: string; customer: string; status: string; due: Date }
const statuses = [ { value: "draft", label: "Draft" }, { value: "open", label: "Open" }, { value: "paid", label: "Paid" }, { value: "void", label: "Void" },]
const initial: Invoice[] = [ { id: "INV-2104", customer: "Northwind Logistics", status: "open", due: new Date(2026, 9, 28) }, { id: "INV-2103", customer: "Kestrel Health", status: "open", due: new Date(2026, 10, 4) }, { id: "INV-2101", customer: "Meridian Freight", status: "draft", due: new Date(2026, 10, 12) },]
export default function InlineEditTable() { const [rows, setRows] = React.useState(initial) const update = (id: string, patch: Partial<Invoice>) => new Promise<void>((r) => setTimeout(() => { setRows((all) => all.map((row) => (row.id === id ? { ...row, ...patch } : row))) r() }, 500) )
return ( <div className="w-full max-w-xl overflow-hidden rounded-xl border bg-card shadow-xs"> <Table> <TableHeader> <TableRow> <TableHead className="w-24">Invoice</TableHead> <TableHead>Customer</TableHead> <TableHead className="w-32">Status</TableHead> <TableHead className="w-40">Due</TableHead> </TableRow> </TableHeader> <TableBody> {rows.map((row) => ( <TableRow key={row.id}> <TableCell className="font-mono text-xs text-muted-foreground">{row.id}</TableCell> <TableCell>{row.customer}</TableCell> <TableCell className="py-1"> <InlineEdit type="select" label={`Status of ${row.id}`} value={row.status} options={statuses} onSave={(v) => update(row.id, { status: v ?? "draft" })} /> </TableCell> <TableCell className="py-1"> <InlineEdit type="date" label={`Due date of ${row.id}`} value={row.due} locale="en-US" onSave={(v) => update(row.id, { due: v ?? row.due })} /> </TableCell> </TableRow> ))} </TableBody> </Table> </div> )}Anatomy
<InlineEdit type="select" label="Account owner" value={owner} options={owners} onSave={async (v) => { await api.update({ owner: v }); setOwner(v) }}/>Guidance
Use it when
- Detail panels and tables where people change one field at a time: owner, status, due date, a name.
- type select and date for fields with a fixed set of values; they save on choice, no confirm step.
Avoid
- Several fields that must change together or be checked together. Use a form with a Save button.
- Long text. Use a Textarea in a form or a dedicated editor.
Rules for AI agents
Published in llms-full.txt and the registry, so coding agents follow them when they build with this component.
- Always pass label naming the field (Due date, Account owner). It becomes the accessible name of the trigger and the field.
- Make onSave async and return a message string (or throw) when the server rejects the value; the field stays in edit mode and shows it.
- Only update your state after the save succeeds, inside onSave, so the shown value is always the saved one.
- Use validate for checks that need no server, like required text.
- Give empty fields a placeholder that invites action: Add a due date, Assign an owner.
API
InlineEdit
| Prop | Type | Default | Description |
|---|---|---|---|
| type | "text" | "number" | "select" | "date" | "text" | Which editor opens. |
| value | string | number | Date | null | The saved value, matching the type. | |
| onSave | (value) => void | string | Promise<void | string> | Persist the value. Return a message or throw to reject it. | |
| label | string | The field's name for assistive tech. | |
| placeholder | string | Shown when the value is empty. | |
| validate | (value) => string | null | Text only. Client-side check before saving. | |
| min / max / step / format / unit / locale | number props | Number only. Passed to Number input. | |
| options | { value, label }[] | Select only. | |
| locale / formatDate / disabledDates | date props | Date only. How the date reads and which days can be picked. |