Skip to content
Component installs need the registry setup
VegaStack Design

Field Cell

One display and one editor per field type, for a grid cell, a panel or a form — so every surface shows and edits a typed value the same way.

Status
stable
Since
0.25.72
Accessibility pattern
named editors; Escape cancels, Enter commits text

Last updated

Name
ARC-120 sample for the Leeds showroom refit
Notes
Ship with the **matte** finish.
Qty
2
Price
€1,240.50
Discount
12.5%
Due
Oct 28, 2026
Visit
Tomorrow · 3:00 PM
Status
Sent
Tags
RequestedSent2 more: Received, Closed
Sample sent
Owner
ARAsha Rao
Customer
Acme LightingRestrictedDeleted
Spec
arc-120.pdf
Total
—
Ref
SR-12
Created by
BKBen Kim

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Field Cell before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify field-cell

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/field-cell

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/badge, @vegastack/button, @vegastack/checkbox, @vegastack/chip, @vegastack/date-picker, @vegastack/date-time, @vegastack/editable-cell, @vegastack/file-kind, @vegastack/inline-chip, @vegastack/input, @vegastack/number-field, @vegastack/people-picker, @vegastack/person-avatar, @vegastack/popover, @vegastack/relative-time, @vegastack/searchable-select, @vegastack/skeleton, @vegastack/text-edit, @vegastack/tooltip, @vegastack/truncated-text, @vegastack/use-async-search.

Usage

import {
  FieldCellDisplay,
  FieldCellEditor,
  FieldInput,
} from "@/components/ui/field-cell";

// Show a value
<FieldCellDisplay type="currency" value={1240.5} config={{ currency: "EUR" }} />

// Edit it in a grid cell: the column's render is the display, the custom editor gets the row
render: (row) => <FieldCellDisplay type="single_choice" value={row.status} config={{ options }} />,
editable: {
  type: "custom",
  render: ({ row, value, seed, commit, cancel }) => (
    // The editor commits a TYPED value: save it, then leave the slot (and move, with the key).
    <FieldCellEditor type="single_choice" label="Status" value={row.status}
      config={{ options }} seed={seed}
      onCommit={(v, move) => { save(row.id, "status", v); commit(value, move); }}
      onCancel={cancel} />
  ),
}

// Edit it in a panel or a form
<FieldInput type="date" aria-label="Due" value={due} onChange={setDue} />;

field-cell is one display and one editor per field type, so a table's cells, a row panel and a record page's custom fields agree. FieldCellDisplay shows a value on one line; FieldCellEditor is what a grid cell opens and commits a typed value (not a string); FieldInput is the same editor as a plain form control.

TypeDisplayEditor
textOne line, truncated (full value on hover and keyboard focus)Text field (maxLength, 10,000 by default)
long_textThe first lineThe TextEdit editor (Markdown)
number, currency, percentEnd-aligned tabular figures in the reader's localeNumberField — "1,5" is 1.5 where the comma is the decimal
dateA calendar day that never shifts with the zoneDatePicker
datetimeDate and time in the reader's zoneDatePicker and a time field
single_choice, multi_choiceFieldChoiceBadges in the option's hue, "+N" past twoThe option list, Add option with onCreateOption
checkboxA checkbox that toggles in place (onToggle)A checkbox
personAvatar and name; several as avatarsPeoplePicker
linkInlineChips in four states, "+N" past threeAn async record search (searchRecords)
fileKind tiles, "+N" past threeUploads live where files are added
url, email, phoneA link out (http(s), mailto, tel)A checked text field
calculatedThe value, or "—" with the missing inputs on hover—
autonumber, created_*, updated_*Read-only—

Field types are data types. Everything a host knows — the options, the people, how records are found, a link kind's icon and noun (the host's InlineChip kinds) — comes in through config and the host's providers. Locale and time zone come from the reader (TimeZoneProvider).

Scope

BehaviourWhere it lives
Saving, required fields, conflictsHost
Formulas, rollups, lookupsHost (formulas compute on the server)
File uploadsThe host's upload area (UploadPanel)

Anatomy

FieldCellDisplay — data-slot="field-cell-display" | "field-cell-filling"
FieldCellEditor — data-slot="field-cell-editor"
FieldChoiceBadge
FieldInput — data-slot="field-input-read-only"

Examples

Choice values

A choice value is a Badge filled in its option's hue — Badge's custom colour route on the tag tokens, never an outlined pill — and FieldChoiceBadge is that badge, so every surface draws an option the same way: a cell, a phone card, the editor's list, a form trigger. Use it wherever the host shows an option outside a field cell (a board card, a filter value). A cell shows two and "+N"; a cut name and the hidden ones show on hover and keyboard focus.

import { FieldChoiceBadge } from "@/components/ui/field-cell";

<FieldChoiceBadge hue="green">
  <span className="min-w-0 truncate">Sent</span>
</FieldChoiceBadge>;

In a grid the choice editor puts no field in the cell: the cell keeps its badges where they were, and the option list opens under it at once — the search field first, each option as its badge, a check on the chosen ones, and "Add “…”" at the end when config.onCreateOption is given. A single choice commits on the pick. A multiple choice toggles with the list open and commits on

Tab, on Enter with no option highlighted, or when the list closes (a click away); Escape cancels. As a form control (FieldInput, a record's property row) a choice is a SearchableSelect field with the same badges in its list.

Editors as form controls

Name
Qty
Price
Due
Visit
Status
Tags
Sample sent
Site

A cell editor

A picker commits on choosing (chosen with Enter, it also moves down) — a multiple choice when the edit ends, as above; text, numbers and date-times commit on Enter or when focus leaves the editor (not into its own calendar); long text opens in a popover, with the @ mention menu when config.mentions is given (with mentionHref and mentionImage, as TextEdit takes them; the row panel's FieldInput too); Escape cancels. The editor takes focus as it opens — a picker's list or calendar opens with it, and long text's editor takes the caret. In a grid it follows the spreadsheet keys:

Enter commits and moves down (Shift+Enter up), Tab right and Shift+Tab left — through onCommit's move — and Tab first steps through a date-time's own date and time. The key that opened it (seed) replaces a text, URL, email, phone, number or long-text value (keys typed while long text's editor loads follow it); a date opens as a field that takes a typed date ("12/3", "Mar 12", "today") starting from it; and a choice, person or link picker opens searching for it. A date-time opens as it is. While an IME is composing, no key commits or moves.

API Reference

PropTypeDefaultDescription
type*FieldType—The field's type.
value*FieldValue—The value, in its type's shape.
classNamestring—Extra classes for the root.
configFieldConfig{}The field's configuration (options, currency, prefix…).
onToggle((next: boolean) => void)—A checkbox toggles where it is shown, with no edit mode: called with the next value. Without it the checkbox is read-only.
state"filling" | "ready" | "recalculating""ready"filling — a converted copy's field still being filled (a shimmer); recalculating — a calculated field being recomputed.

Data attributes and CSS variables on FieldCellDisplay

AttributeValues
data-slot"field-cell-display" | "field-cell-filling"
data-typemirrors a prop or state value

FieldInput

PropTypeDefaultDescription
aria-label*string—The control's accessible name (its field's name).
onChange*(value: FieldValue) => void—Called with each new typed value.
type*FieldType—The field's type.
value*FieldValue—The value, in its type's shape.
aria-describedbystring—Points at the host's error text.
autoFocusbooleanfalseFocus the control when it mounts (an opened cell editor).
classNamestring—Extra classes for the control.
configFieldConfig{}The field's configuration (options, people, record search…).
defaultSearchstring—A picker (choice, person, link) opens on mount searching for this text — the character that opened a cell editor. Other types ignore it.
disabledbooleanfalseDisable the control.
idstring—The control's id (a label's htmlFor).
invalidbooleanfalseMark the control invalid (the host's validation).

Data attributes and CSS variables on FieldInput

AttributeValues
data-slot"field-input-read-only"

FieldCellEditor

PropTypeDefaultDescription
label*string—The field's name, for the editor's accessible name.
onCancel*() => void—Close without committing.
onCommit*(value: FieldValue, move?: EditableCellMove) => void—Commit a typed value and close. move is where the key that committed sends the grid's focus (<kbd>Enter</kbd> down, <kbd>Shift</kbd>+<kbd>Enter</kbd> up, <kbd>Tab</kbd> right, <kbd>Shift</kbd>+<kbd>Tab</kbd> left); a pick, a blur or the long-text editor's save has none. Hand it on to the slot's commit(value, move).
type*FieldType—The field's type.
value*FieldValue—The value the cell holds.
configFieldConfig{}The field's configuration (options, people, record search…).
seedstring—The character that opened the editor (the slot's seed): a text, URL, email, phone, number or long-text field starts from it in place of the value, a date opens as a field that takes a typed date ("12/3", "Mar 12", "today") starting from it, and a picker (choice, person, link) opens searching for it. A date-time opens as it is.

Data attributes and CSS variables on FieldCellEditor

AttributeValues
data-slot"field-cell-editor"
data-typemirrors a prop or state value

Config

PropTypeDefaultDescription
currencystring"USD"A currency field's ISO code.
decimalsnumberthe value's ownDecimal places a number, currency or percent shows.
maxLengthnumber10000A text field's most characters.
mentionHref((kind: MentionKind, id: string) => string | null)—Where a mention in a long-text field links (TextEdit's mentionHref).
mentionImage((id: string) => string | null | undefined)—A person mention's photo in a long-text field (TextEdit's mentionImage).
mentionsTextEditMentionsundefined — no mention menuA long-text field's @ mentions: the menu's kinds and search, as TextEdit takes them.
missingInputsreadonly string[]—A calculated field's inputs that are empty, shown on hover over "—".
multiplebooleanfalseA person field holds several.
onCreateOption((label: string) => Promise<FieldChoiceOption>)—Lets a choice editor add the typed option ("Add “Sent”"); resolves with the created option.
optionsreadonly FieldChoiceOption[]—A choice field's options, in order.
peoplereadonly FieldPerson[]—The people a person field offers (or searchPeople).
placeholderstring—Shown while an editor or input is empty.
prefixstring""An auto-number's prefix ("SR-").
searchPeoplePeoplePickerSearch—Finds people for a person field.
searchRecordsAsyncSearchLoader<FieldLinkTarget>—Finds records for a link field (recents first is the host's order).

FieldChoiceBadge

PropTypeDefaultDescription
hueChipHue"neutral"The option's hue (FieldChoiceOption.hue).

Data attributes and CSS variables on FieldChoiceBadge

AttributeValues
data-huemirrors a prop or state value

Accessibility

  • Every editor is named by its field (aria-label / label) and marks a value it refuses aria-invalid (an invalid URL, email or phone).
  • Each link chip stays on one line and is never broken inside a name, even inside a host's prose; a cell that grows (className="flex-wrap", a page's table block) wraps whole chips.
  • A truncated text, number, date, tag or link name is revealed on hover and on keyboard focus; a link list's "+2" names what it hides on hover and on keyboard focus (an unreachable link only by its state); every overflowed list (links, people, files) is read in full by screen readers; the row panel shows every value whole.
  • A restricted link is muted and never a link; an unavailable or deleted one says so.
KeyAction
EnterCommit a text, number, URL, email or phone editor; move down.
Enter in a choice listPick the highlighted option (a multiple choice toggles it).
Shift+EnterCommit and move up.
Tab / Shift+TabCommit and move right / left (past the editor's own controls).
⌘/Ctrl+EnterCommit long text.
EscapeCancel the edit.
ContractStates tested
Behaviourdisplay, editing, empty, filling, recalculating, read-only, invalid
Accessibilitykeyboard, labeled, aria-invalid
Visualtext, number, date, tags, checkbox, person, link-states, file-tiles, link-out

Do / Don't

Do
Pass the field's options, people and record search in config, and let FieldCellDisplay show every surface the same way.
Don't
Write a field editor in a module — a new type belongs here, once.

On this page