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
- 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
- Tags
- 2 more: Received, Closed
- Sample sent
- Owner
- ARAsha Rao
- Customer
- Acme LightingRestricted
Deleted - Spec
- arc-120.pdf
- Contact
- asha@example.com
- Phone
- +44 20 7946 0958
- 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-cellInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/field-cellRun 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.
| Type | Display | Editor |
|---|---|---|
text | One line, truncated (full value on hover and keyboard focus) | Text field (maxLength, 10,000 by default) |
long_text | The first line | The TextEdit editor (Markdown) |
number, currency, percent | End-aligned tabular figures in the reader's locale | NumberField — "1,5" is 1.5 where the comma is the decimal |
date | A calendar day that never shifts with the zone | DatePicker |
datetime | Date and time in the reader's zone | DatePicker and a time field |
single_choice, multi_choice | FieldChoiceBadges in the option's hue, "+N" past two | The option list, Add option with onCreateOption |
checkbox | A checkbox that toggles in place (onToggle) | A checkbox |
person | Avatar and name; several as avatars | PeoplePicker |
link | InlineChips in four states, "+N" past three | An async record search (searchRecords) |
file | Kind tiles, "+N" past three | Uploads live where files are added |
url, email, phone | A link out (http(s), mailto, tel) | A checked text field |
calculated | The 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
| Behaviour | Where it lives |
|---|---|
| Saving, required fields, conflicts | Host |
| Formulas, rollups, lookups | Host (formulas compute on the server) |
| File uploads | The host's upload area (UploadPanel) |
Anatomy
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
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
| Prop | Type | Default | Description |
|---|---|---|---|
type* | FieldType | — | The field's type. |
value* | FieldValue | — | The value, in its type's shape. |
className | string | — | Extra classes for the root. |
config | FieldConfig | {} | 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
| Attribute | Values |
|---|---|
data-slot | "field-cell-display" | "field-cell-filling" |
data-type | mirrors a prop or state value |
FieldInput
| Prop | Type | Default | Description |
|---|---|---|---|
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-describedby | string | — | Points at the host's error text. |
autoFocus | boolean | false | Focus the control when it mounts (an opened cell editor). |
className | string | — | Extra classes for the control. |
config | FieldConfig | {} | The field's configuration (options, people, record search…). |
defaultSearch | string | — | A picker (choice, person, link) opens on mount searching for this text — the character that opened a cell editor. Other types ignore it. |
disabled | boolean | false | Disable the control. |
id | string | — | The control's id (a label's htmlFor). |
invalid | boolean | false | Mark the control invalid (the host's validation). |
Data attributes and CSS variables on FieldInput
| Attribute | Values |
|---|---|
data-slot | "field-input-read-only" |
FieldCellEditor
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
config | FieldConfig | {} | The field's configuration (options, people, record search…). |
seed | string | — | 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
| Attribute | Values |
|---|---|
data-slot | "field-cell-editor" |
data-type | mirrors a prop or state value |
Config
| Prop | Type | Default | Description |
|---|---|---|---|
currency | string | "USD" | A currency field's ISO code. |
decimals | number | the value's own | Decimal places a number, currency or percent shows. |
maxLength | number | 10000 | A 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). |
mentions | TextEditMentions | undefined — no mention menu | A long-text field's @ mentions: the menu's kinds and search, as TextEdit takes them. |
missingInputs | readonly string[] | — | A calculated field's inputs that are empty, shown on hover over "—". |
multiple | boolean | false | A person field holds several. |
onCreateOption | ((label: string) => Promise<FieldChoiceOption>) | — | Lets a choice editor add the typed option ("Add “Sent”"); resolves with the created option. |
options | readonly FieldChoiceOption[] | — | A choice field's options, in order. |
people | readonly FieldPerson[] | — | The people a person field offers (or searchPeople). |
placeholder | string | — | Shown while an editor or input is empty. |
prefix | string | "" | An auto-number's prefix ("SR-"). |
searchPeople | PeoplePickerSearch | — | Finds people for a person field. |
searchRecords | AsyncSearchLoader<FieldLinkTarget> | — | Finds records for a link field (recents first is the host's order). |
FieldChoiceBadge
| Prop | Type | Default | Description |
|---|---|---|---|
hue | ChipHue | "neutral" | The option's hue (FieldChoiceOption.hue). |
Data attributes and CSS variables on FieldChoiceBadge
| Attribute | Values |
|---|---|
data-hue | mirrors a prop or state value |
Accessibility
- Every editor is named by its field (
aria-label/label) and marks a value it refusesaria-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.
| Key | Action |
|---|---|
| Enter | Commit a text, number, URL, email or phone editor; move down. |
| Enter in a choice list | Pick the highlighted option (a multiple choice toggles it). |
| Shift+Enter | Commit and move up. |
| Tab / Shift+Tab | Commit and move right / left (past the editor's own controls). |
| ⌘/Ctrl+Enter | Commit long text. |
| Escape | Cancel the edit. |
| Contract | States tested |
|---|---|
| Behaviour | display, editing, empty, filling, recalculating, read-only, invalid |
| Accessibility | keyboard, labeled, aria-invalid |
| Visual | text, number, date, tags, checkbox, person, link-states, file-tiles, link-out |