Skip to content

Record inline edit

A record's Details tab, edited in place with a pencil, Cancel and Save. The app does the saving.

Opportunity Owner
Amount
$75,000.00
Private
False
Expected Revenue
$56,250.00
Opportunity Name
Edge Emergency Generator
Close Date
Nov 20, 2026
Account Name
Next Step
Send the revised quote
Type
Existing Customer - Upgrade
Stage
Proposal/Price Quote
Lead Source
Partner Referral
Probability (%)
75%

Order Number
00047181
Main Competitor(s)
Generac, Kohler
Current Generator(s)
Two 500 kW diesel units
Delivery/Installation Status
Yet to begin
Installation Start
Dec 1, 2026, 3:00 PM
Tracking Number
1Z 999 AA1 01 2345 6784
Description
Backup power for the Austin data center. Site survey done on September 30.

Last Modified Date
Oct 9, 2026, 4:42 PM

Installation

pnpm dlx shadcn@latest add @wisty/record-inline-edit

The CLI installs the Radix or Base UI file to match your project, with what it builds on: button, icon, record-detail, record-form.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • A record page's Details tab where people change fields in place: Lightning's inline edit, a pencil on each field, then Cancel and Save at the bottom.
  • The fields, which are editable and the picklist values come from the object's metadata and page layout, and the app saves through updateRecord or the REST API.
  • What lightning-record-form draws in view mode with its edit pencils.

Don't use it

  • The tab is read-only, or a pencil opens a form elsewhere. Use Record detail.
  • People create a record, or edit it in a modal. Use Record form.
  • Many records side by side. Use Data table.

Usage

import {
  RecordInlineEdit,
  RecordInlineEditCancel,
  RecordInlineEditField,
  RecordInlineEditFooter,
  RecordInlineEditMessages,
  RecordInlineEditSection,
} from "@/components/wisty/record-inline-edit"
import { RecordFormSubmit } from "@/components/wisty/record-form"
RecordInlineEdit
├── RecordInlineEditMessages
├── RecordInlineEditSection
│   └── RecordInlineEditField
└── RecordInlineEditFooter
    ├── RecordInlineEditCancel
    └── RecordFormSubmit

fields describes every field, keyed by API name: Record form's description, which extends Record detail's, with editable: false for a field people can't change and href for a lookup's link. values is the record. The same two render a read-only Record detail. Each RecordInlineEditField draws one field by name. Saving is the app's, so control mode: onSubmit gets the values, and the app passes pending and a failed save's errors back in, or the new values with mode set to "view". defaultMode only sets the mode it starts in.

const [mode, setMode] = useState<RecordInlineEditMode>("view")
const [values, setValues] = useState(record)
const [errors, setErrors] = useState<RecordFormErrors>()
const [pending, startSaving] = useTransition()

<RecordInlineEdit
  fields={fields}
  values={values}
  mode={mode}
  onModeChange={setMode}
  pending={pending}
  errors={errors}
  onSubmit={(_, { changed }) =>
    startSaving(async () => {
      const result = await updateOpportunity(changed)
      setErrors(result.errors)
      if (result.errors) return
      setValues(result.record)
      setMode("view")
    })
  }
>
  …
</RecordInlineEdit>

Send changed, the values that differ from values, to updateRecord. When it's empty, set the mode back to "view" without a request. To lay out a page layout loaded at runtime, map its sections to RecordInlineEditSection and their field names to RecordInlineEditField, as Record form's Usage does.

RecordInlineEditFooter draws Cancel and Save on its own. To order the buttons or add one of the page's, pass them as children: RecordInlineEditCancel returns to view mode, and Save is Record form's RecordFormSubmit:

<RecordInlineEditFooter>
  <RecordInlineEditCancel />
  <Button type="button" variant="outline" onClick={openFieldHistory}>
    Field History
  </Button>
  <RecordFormSubmit>Save</RecordFormSubmit>
</RecordInlineEditFooter>

A field whose control Record form doesn't draw, like a multi-select picklist, takes a custom control as RecordInlineEditField's child, written with useRecordFormControl as in Record form. Edit mode draws it; view mode shows the value by the field's type.

The footer sticks to the bottom of its scroll container. For Lightning's footer, fixed to the bottom of the window, set it with className:

<RecordInlineEditFooter className="fixed inset-x-0 bottom-0 z-50" />

Examples

Editing

With `defaultMode="edit"`: Owner and Expected Revenue stay read-only with a lock, and a field you change turns yellow with an undo button.

Opportunity Owner
Locked Opportunity Owner
$
Expected Revenue
$56,250.00
Locked Expected Revenue
%

After a failed save

The app's `errors`: the account lookup's filter error on its field, and a validation rule's message for the record above the sections.

Opportunity Owner
Locked Opportunity Owner
$
Expected Revenue
$56,250.00
Locked Expected Revenue
%

Saving

With `pending`, Save shows a spinner and is disabled, so it can't save twice, while the app saves the changed values.

Opportunity Owner
Locked Opportunity Owner
$
Expected Revenue
$56,250.00
Locked Expected Revenue
%

Compact density

With `layout="horizontal"` and `columns={1}`, labels sit beside values, as in Lightning's compact density, shown in edit mode with its locks.

Opportunity Owner
Locked Opportunity Owner
$
Expected Revenue
$56,250.00
Locked Expected Revenue
%

Read-only record

With `readOnly`, for a record people can see but not edit: the same fields, and no pencils.

Opportunity Owner
Amount
$75,000.00
Private
False
Expected Revenue
$56,250.00
Opportunity Name
Edge Emergency Generator
Close Date
Nov 20, 2026
Next Step
Send the revised quote
Type
Existing Customer - Upgrade
Stage
Proposal/Price Quote
Lead Source
Partner Referral
Probability (%)
75%

Order Number
00047181
Main Competitor(s)
Generac, Kohler
Current Generator(s)
Two 500 kW diesel units
Delivery/Installation Status
Yet to begin
Installation Start
Dec 1, 2026, 3:00 PM
Tracking Number
1Z 999 AA1 01 2345 6784
Description
Backup power for the Austin data center. Site survey done on September 30.

Loading

With `loading` and no values yet, the layout's labels hold their places and every value is a placeholder.

Opportunity Owner
Amount
Private
Expected Revenue
Opportunity Name
Close Date
Account Name
Next Step
Type
Stage
Lead Source
Probability (%)

Order Number
Main Competitor(s)
Current Generator(s)
Delivery/Installation Status
Installation Start
Tracking Number
Description

API reference

PartBuilt onDescription
RecordInlineEdit<div> (opens in a new tab)
RecordInlineEditMessagesRecordFormMessagesThe errors for the whole record, above the sections in edit mode. Renders nothing without any.
RecordInlineEditSectionRecordDetailSectionA collapsible section of the page layout, drawn the same in both modes.
RecordInlineEditField<div> (opens in a new tab)One field by API name: its value with a pencil in view mode, the form field or a locked value in edit mode.
RecordInlineEditFooter<div> (opens in a new tab)The buttons docked at the bottom of the block in edit mode: Cancel and Save, or its children.
RecordInlineEditCancelButtonCancel: back to view mode, dropping the edits. Disabled while the save runs.

RecordInlineEdit

PropTypeDefaultDescription
fields*RecordInlineEditFieldsEvery field the layout can show, keyed by API name.
valuesRecordInlineEditValues{}The record's values. After a save, pass the new values with mode "view".
modeRecordInlineEditModeView or edit, controlled. A save needs it controlled, so the app can return to view.
defaultModeRecordInlineEditMode"view"The mode it starts in, uncontrolled. Default "view".
onModeChange(mode: RecordInlineEditMode) => voidCalled when a pencil asks for edit mode, or Cancel for view mode.
pendingbooleanfalseThe save is running: Save shows a spinner and is disabled.
errorsRecordFormErrorsA failed save's errors. Pass a new object for each save, from state.
onSubmit( values: RecordFormValues, details: RecordInlineEditSubmitDetails ) => voidCalled with valid values when Save is pressed.
readOnlybooleanfalseThe user can't edit the record: no pencils, and view mode only.
columns1 | 22Fields per row. Default 2. Two columns fall to one in a narrow container.
layout"auto" | "stacked" | "horizontal""auto"Label above the value, or beside it from a 48rem screen. Default "auto": beside it when the page is compact (data-density="compact"), above it otherwise.
headingLevel2 | 3 | 42The level of each section's heading. Default 2, under a record page's h1.
loadingbooleanfalseHolds every value's place while the record loads, with no pencils. Without children, draws placeholder fields.
localestring"en-US"A BCP 47 locale for numbers and dates. Default "en-US".
currencystring"USD"An ISO 4217 code for currency fields. Default "USD".
timeZonestring"UTC"An IANA time zone whose clock datetime fields show and are edited in. Default "UTC".

RecordInlineEditSection

A collapsible section of the page layout, drawn the same in both modes.

PropTypeDefaultDescription
valuestringA stable key for its open state, like the layout section's id. Without it, the title keys it.
defaultOpenbooleantrueWhether the section starts open, uncontrolled. Default true.

RecordInlineEditField

One field by API name: its value with a pencil in view mode, the form field or a locked value in edit mode.

PropTypeDefaultDescription
childrenReact.ReactNodeA custom control for edit mode that calls useRecordFormControl; omit it for the type's own.
name*stringThe field's API name, a key of the root's fields.

RecordInlineEditFooter

The buttons docked at the bottom of the block in edit mode: Cancel and Save, or its children.

PropTypeDefaultDescription
childrenReact.ReactNodeButtons in place of Cancel and Save; leave it out for the defaults.
cancelLabelstring"Cancel"The Cancel button's text, without children. Default "Cancel".
saveLabelstring"Save"The Save button's text, without children. Default "Save".

RecordInlineEditCancel

Cancel: back to view mode, dropping the edits. Disabled while the save runs.

PropTypeDefault
childrenReactNode"Cancel"
variant"default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | "outline-brand" | "destructive-text" | "success" | "inverse""outline"

RecordInlineEditFieldDescriptiontype

How a field is drawn, read and edited: Record form's description, or a relative time, which is only shown.

type RecordInlineEditFieldDescription =
  | RecordFormFieldDescription
  | Extract<RecordDetailFieldDescription, { type: "relative" }>

RecordInlineEditFieldstype

The record's fields, keyed by API name.

type RecordInlineEditFields = Record<string, RecordInlineEditFieldDescription>

RecordInlineEditValuestype

The record's values, keyed by API name: a lookup's is its record, a compound field's its parts, an instant ISO 8601.

type RecordInlineEditValues = RecordDetailValues

RecordInlineEditModetype

type RecordInlineEditMode = "view" | "edit"

RecordInlineEditSubmitDetailstype

MemberTypeDefaultDescription
changed*RecordFormValuesThe values that differ from values, as updateRecord takes them.

SLDS mapping

  • RecordInlineEditSection is Record detail's section, an expandable slds-section over the record detail form layout (slds-form, slds-form__row, slds-form__item), drawn the same in both modes.
  • In view mode, RecordInlineEditField is Record detail's field: slds-form-element_readonly with slds-form-element_edit and its pencil. In edit mode it's Record form's field, and a field people can't edit stays read-only with utility:lock where the pencil was.
  • A changed field is slds-is-edited, with slds-form-element__undo's utility:undo button.
  • RecordInlineEditMessages is Record form's error Alert.
  • RecordInlineEditFooter is slds-docked-form-footer, sticky at the bottom of its scroll container rather than fixed to the window.

Known gaps:

  • No multi-select picklist or rich text controls in edit mode, as in Record form. Draw them with a custom control. A relative time ("3 hours ago") stays read-only.
  • No error popover next to the footer's buttons; the record's errors show above the sections.
  • A pencil opens every editable field, as Lightning's record detail does. Editing one field alone, as a list view's cell does, isn't drawn.

Keyboard interactions

Key Action
Enter on a pencil Switches to edit mode and focuses that field's control.
Enter in a field Saves, as Save does. In a lookup or picklist with its list open, Enter picks the option.

Accessibility

  • Each section's title is a heading with its toggle inside: h2 by default, for a record page whose header is the h1. Under another heading, set headingLevel one level below it.
  • A pencil is named after its field, like "Edit Amount", an undo button "Undo Amount", and a lock "Locked Opportunity Owner".
  • Cancel, or a save the app finishes by setting the mode to "view", returns focus to the pencil that started the edit, or to its section's toggle when that section was collapsed meanwhile.
  • While pending, Save and Cancel are disabled, so the save's answer can't land on a newer edit. Fields stay editable.
  • A save that fails moves focus to the first field with an error, or to the record's errors when none belongs to a field on the page, and opens every collapsed section until the edit ends.

Responsive behavior

  • Two columns need about 584px. In a narrower container the fields stack in one column, in the same order.
  • Labels follow Salesforce's display density by default (layout="auto"): above the values, and beside them when the page is compact (data-density="compact" on <html>). layout="stacked" or "horizontal" fixes it for one block.
  • Beside means from a 48rem screen, whatever the block's own width; below that they stack.

Rules

  • Control mode for a save: the block never leaves edit mode on its own, and never reads success from pending going false.
  • Pass errors from state, a new object for each failed save. Errors left over from an earlier edit don't show when a new one starts.
  • Pass null for an empty value, never "" or 0. A percent is a fraction: 0.6 for 60%. A lookup's value is its record ({ id, name }).
  • A relative field must be editable: false. A datetime is an instant, shown and edited on timeZone's clock, so pass the user's time zone; a time is Salesforce's "HH:mm:ss.SSSZ", which no time zone moves.
  • Give sections with the same title a value, or they share their open state.
  • Keep the footer inside RecordInlineEdit: Save is a submit button. It and its children show only in edit mode. With children, keep a RecordFormSubmit and a RecordInlineEditCancel among them, or people can't save or leave edit mode.
  • Never pass asChild or render.