Skip to content

Record detail

A record's Details tab, read-only, for a standard or custom object. Same field descriptions as Record form and Record inline edit.

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
Tracking Number
1Z 999 AA1 01 2345 6784
Description
Backup power for the Austin data center. Site survey done on September 30.

Installation

pnpm dlx shadcn@latest add @wisty/record-detail

The CLI installs the Radix or Base UI file to match your project, with what it builds on: expandable-section, lookup, output-field.

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: the fields of one record in the page layout's sections, the way Lightning's record detail shows an opportunity, an account or a custom object's record.
  • A pencil on each field opens an edit elsewhere, like a modal with Record form. onEdit says which field was pressed.
  • The same field descriptions serve Record form and Record inline edit, so one set covers view, edit and inline edit.

Don't use it

  • One field, or your own arrangement of fields. Use Output field.
  • People edit the fields in place, with Cancel and Save at the bottom. Use Record inline edit.
  • People create or edit the record in a form. Use Record form.
  • Many records side by side. Use Data table.

Usage

import {
  RecordDetail,
  RecordDetailField,
  RecordDetailSection,
} from "@/components/wisty/record-detail"
RecordDetail
└── RecordDetailSection
    └── RecordDetailField

fields describes every field, keyed by API name: its label, type and help, with editable: false for a field people can't change and href for a lookup's link. A description also takes the Output field format keys its type reads: scale, minimumFractionDigits and maximumFractionDigits on a number, plus currencyDisplay on a currency, dateStyle on a date, the date and time styles, hour12 and timeZoneName on a datetime, and linkLabel, linkTarget and linkTitle on an email, phone or URL. Record form and Record inline edit take the same descriptions, with what editing needs added. values is the record. Each RecordDetailField draws one field by name, its value formatted by the field's type.

<RecordDetail
  fields={{
    Name: { label: "Account Name", type: "text" },
    OwnerId: {
      label: "Account Owner",
      type: "lookup",
      href: (record) => `/users/${record.id}`,
      editable: false,
    },
    AnnualRevenue: { label: "Annual Revenue", type: "currency", scale: 0 },
    LastModifiedDate: {
      label: "Last Modified Date",
      type: "relative",
      editable: false,
    },
  }}
  values={{
    Name: "Edge Communications",
    OwnerId: { id: "005Dn00000EllenH", name: "Ellen Hart" },
    AnnualRevenue: 139000000,
    LastModifiedDate: "2026-10-13T12:00:00Z",
  }}
  now={now}
  onEdit={(name) => openEditForm(name)}
>
  <RecordDetailSection title="Account Information">
    <RecordDetailField name="Name" />
    <RecordDetailField name="OwnerId" />
    <RecordDetailField name="AnnualRevenue" />
  </RecordDetailSection>
  <RecordDetailSection title="System Information" defaultOpen={false}>
    <RecordDetailField name="LastModifiedDate" />
  </RecordDetailSection>
</RecordDetail>

Fields fill each row left to right, then down, as a page layout's rows do. To lay out a page layout loaded at runtime, map its sections to RecordDetailSection and their field names to RecordDetailField, as Record form's Usage does.

Examples

Account with addresses

An account's Billing and Shipping Address on their lines, a missing country left out, and Last Modified Date as `type="relative"` from a fixed `now`.

Account Owner
Account Name
Edge Communications
Type
Customer - Direct
Annual Revenue
$139,000,000

Billing Address
312 Constitution Place Austin, TX 78767 USA
Shipping Address
312 Constitution Place Loading Dock B Austin, TX 78767

Last Modified Date
, Oct 13, 2026, 8:00 AM
Created Date
Feb 3, 2026, 11:20 AM

One column

The same record with `columns={1}` and `layout="horizontal"`, labels beside values, as for a narrow region or compact density.

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
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 labels stay and each value is a placeholder until the record arrives.

Opportunity Owner
Amount
Opportunity Name
Close Date
Account Name
Stage

Empty fields

A new opportunity whose Amount, Next Step, Type and Lead Source are `null`: they render blank, as an empty field does.

Opportunity Owner
Amount
Opportunity Name
Edge Emergency Generator
Close Date
Nov 20, 2026
Account Name
Next Step
Type
Stage
Prospecting
Lead Source

API reference

PartBuilt onDescription
RecordDetail<div> (opens in a new tab)A record's fields, read-only, in the page layout's collapsible sections.
RecordDetailSection<div> (opens in a new tab)A collapsible section of the page layout.
RecordDetailField<div> (opens in a new tab)One field by API name: its label and formatted value, with a pencil when onEdit is set.

RecordDetail

A record's fields, read-only, in the page layout's collapsible sections.

PropTypeDefaultDescription
fields*RecordDetailFieldsEvery field the layout can show, keyed by API name.
valuesRecordDetailValues{}The record's values, keyed by API name. A missing or null value shows blank.
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.
loadingbooleanfalseHolds every value's place while the record loads. Without children, draws placeholder fields.
headingLevel2 | 3 | 42The level of each section's heading. Default 2, under a record page's h1.
onEdit(name: string) => voidCalled with the field's API name when its pencil is pressed. Without it, no pencils show.
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 for datetime fields. Default "UTC".
nowstring | number | DateThe instant relative fields count from, as ISO 8601. Defaults to the current time at render.

RecordDetailSection

A collapsible section of the page layout.

PropTypeDefaultDescription
title*stringThe section's heading, as the page layout names it.
defaultOpenbooleantrueWhether the section starts open, uncontrolled. Default true.
openbooleanWhether the section is open, controlled.
onOpenChange(open: boolean) => voidCalled when the toggle opens or closes the section.
columns1 | 2Fields per row, over the root's columns.

RecordDetailField

One field by API name: its label and formatted value, with a pencil when onEdit is set.

PropTypeDefaultDescription
childrenReact.ReactNodeDrawn after the value and its pencil, like a lock on a field people can't edit.
name*stringThe field's API name, a key of the root's fields.

RecordDetailOptiontype

A picklist value and its label.

MemberTypeDefaultDescription
value*stringWhat's stored.
label*stringWhat's shown.

RecordDetailFieldDescriptiontype

What a field is and how it reads; Record form's description extends it for editing.

type RecordDetailFieldDescription = RecordDetailFieldBase &
  (
    | { type: "text" | "textarea" }
    | ({ type: "email" | "phone" | "url" } & OutputFieldLinkOptions)
    | ({ type: "number" | "percent" } & RecordDetailDigits)
    | ({ type: "currency" } & RecordDetailDigits &
        Pick<OutputFieldStyleOptions, "currencyDisplay">)
    | ({
        type: "date"
      } & Pick<OutputFieldStyleOptions, "dateStyle">)
    | {
        type: "picklist"
        options: readonly RecordDetailOption[]
      }
    | { type: "boolean" }
    | {
        type: "address"
      }
    | {
        type: "name"
      }
    | {
        type: "lookup"
        records?: readonly LookupRecord[]
        href?: (record: LookupRecord) => string | undefined
      }
    | ({
        type: "datetime"
      } & Pick<
        OutputFieldStyleOptions,
        "dateStyle" | "timeStyle" | "hour12" | "timeZoneName"
      >)
    | ({
        type: "time"
      } & Pick<OutputFieldStyleOptions, "timeStyle" | "hour12">)
    | ({
        type: "relative"
        editable: false
      } & Pick<OutputFieldStyleOptions, "dateStyle" | "timeStyle" | "hour12">)
  )

RecordDetailValuestype

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 RecordDetailValues = Record<
  string,
  | string
  | number
  | boolean
  | LookupRecord
  | OutputFieldAddress
  | OutputFieldName
  | null
  | undefined
>

RecordDetailFieldstype

The record's fields, keyed by API name.

type RecordDetailFields = Record<string, RecordDetailFieldDescription>

Accessibility

  • Each section's title is a heading with the toggle inside it: h2 by default, for a record page whose header is the h1. Under another heading, set headingLevel one level below it.
  • Each section's fields are a list, one item per field, and each field is a group named by its label. A pencil is named after its field, like "Edit Amount".
  • While loading, the block is busy and every value is a placeholder.

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

  • Name only fields in fields; a name that isn't there throws. Leave out a field the user can't read rather than passing it as null.
  • null is an empty field and renders blank. Never pass 0, a dash or "None" for it.
  • A percent is a fraction: divide a Salesforce percent field's value by 100. A date is a day ("2026-11-20"), a datetime an instant. A lookup's value is its record ({ id, name }).
  • Pass scale from the field's definition, so whole-dollar fields don't show cents. An explicit minimumFractionDigits or maximumFractionDigits wins over it on its end.
  • An address is { street, city, state, postalCode, country } and shows on its lines: the street, then "City, State PostalCode", then the country, a missing part dropping out with its comma. A name is { salutation, firstName, middleName, lastName, suffix } on one line. Both follow Salesforce's en-US order for every country and locale. One with no parts renders blank.
  • A relative field must be editable: false. A datetime value is an instant, shown in timeZone; a time value is a time of day as Salesforce returns it ("09:30:00.000Z"), never moved by timeZone. A relative value is an instant: pass now, one value for the page, so the text stays the same between the server and the browser.