Field types
Every value type a record can hold, as it reads in a 36px table cell, in the details rail and in a form. A type looks the same everywhere (C463). Each opens one of six shared editors, drawn below.
Playground
The picker every select, status, tag, record and person field opens. Change the controls to try each prop; type something that isn't listed to see the create row.
import { Picker } from '@/ui/picker';<Picker label="Job type" />Every type in a cell
Each value type as it reads in a 36px table cell. Numbers, money, percent and duration sit right with tabular figures. Email and phone are ink, not links; hover or focus shows copy and open. A URL shows its host, a LinkedIn link its handle.
- Text
- Senior Data Engineer
- Long text
- Strong on Spark, wants a hybrid role., Strong on Spark, wants a hybrid role. Notice period agreed with Northwind.
- Number
- 1,200
- Currency
- 148,500 GBP
- Percent
- 18%
- Duration
- 3 months
- Checkbox
- Rating
- Progress
- 3 of 5
- Date
- 3 Nov
- Timestamp
- 2h ago, 16 Oct 2026, 12:05
- Select
- Permanent
- Status
- Interview
- Tags
- PythonSparkSQLAirflowdbt
- Person
- Joseph Ariel
- Address
- London, UK
- Record
- Halcyon Bank
import { FieldDisplay, type FieldValue } from '@/ui/field-display';
const rows: [string, FieldValue][] = [
['Text', { type: 'text', value: 'Senior Data Engineer' }],
['Long text', { type: 'long_text', value: 'Strong on Spark, wants a hybrid role.\nNotice period agreed with Northwind.' }],
['Number', { type: 'number', value: 1200 }],
['Currency', { type: 'currency', value: 148500, currency: 'GBP' }],
['Percent', { type: 'percent', value: 18 }],
['Duration', { type: 'duration', value: 129_600 }],
['Checkbox', { type: 'checkbox', value: true }],
['Rating', { type: 'rating', value: 3 }],
['Progress', { type: 'progress', value: 3, max: 5 }],
['Date', { type: 'date', value: '2026-11-03' }],
['Timestamp', { type: 'timestamp', value: '2026-10-16T12:05:00Z' }],
['Select', { type: 'select', value: 'Permanent', color: 'blue' }],
['Status', { type: 'status', value: 'Interview', phase: 'working' }],
['Tags', { type: 'tags', value: ['Python', 'Spark', 'SQL', 'Airflow', 'dbt'] }],
['Person', { type: 'person', value: { name: 'Joseph Ariel' } }],
['Email', { type: 'email', value: 'sara@halcyonbank.com' }],
['Phone', { type: 'phone', value: '+44 20 7946 0958' }],
['URL', { type: 'url', value: 'https://halcyonbank.com/careers' }],
['Social', { type: 'url', value: 'https://www.linkedin.com/in/saralund' }],
['Address', { type: 'address', value: 'London, UK' }],
['Record', { type: 'record', value: { type: 'company', name: 'Halcyon Bank', domain: 'halcyonbank.com' } }],
];
// Each type as it reads in a 36px table cell. Hover the email or phone for copy and open.
export default function EveryType() {
return (
<dl className="grid w-full max-w-120 grid-cols-[8rem_minmax(0,1fr)] text-body leading-body">
{rows.map(([name, field]) => (
<div key={name} className="col-span-2 grid h-9 grid-cols-subgrid items-center border-b border-border">
<dt className="text-text-subtle">{name}</dt>
<dd className="min-w-0">
<FieldDisplay field={field} label={name} today="2026-10-16" now="2026-10-16T14:05:00Z" />
</dd>
</div>
))}
</dl>
);
}The shared editors
Every type opens one of these, in a cell, the details rail or a form. Type in the select menu to filter; a name that isn't listed offers Create at the end. The multi-select shows chosen values as tags in the search row, and Backspace in an empty search removes the last one.
import { useState } from 'react';
import { DatePicker } from '@/ui/date-picker';
import { Field, FieldLabel } from '@/ui/field';
import { Input } from '@/ui/input';
import { Picker, type PickerItem } from '@/ui/picker';
import { RecordMark } from '@/ui/record-chip';
import { Tag } from '@/ui/tag';
const types: PickerItem[] = [
{ value: 'permanent', label: 'Permanent', display: <Tag color="blue" size="sm">Permanent</Tag> },
{ value: 'part-time', label: 'Permanent, part time', display: <Tag color="teal" size="sm">Permanent, part time</Tag> },
{ value: 'contract', label: 'Contract', display: <Tag color="amber" size="sm">Contract</Tag> },
];
const skills: PickerItem[] = ['Python', 'Scala', 'SQL', 'Spark', 'Airflow'].map((skill) => ({ value: skill, label: skill }));
const records: PickerItem[] = [
{ value: 'halcyon', label: 'Halcyon Bank', detail: 'halcyonbank.com', group: 'Companies', display: <span className="flex items-center gap-100"><RecordMark type="company" name="Halcyon Bank" domain="halcyonbank.com" />Halcyon Bank</span> },
{ value: 'halden', label: 'Halden Labs', group: 'Companies', display: <span className="flex items-center gap-100"><RecordMark type="company" name="Halden Labs" />Halden Labs</span> },
{ value: 'hal', label: 'Hal Lindgren', group: 'People', display: <span className="flex items-center gap-100"><RecordMark type="contact" name="Hal Lindgren" />Hal Lindgren</span> },
];
// The shared editors every type opens, in a cell, the details rail or a form.
export default function Editors() {
const [type, setType] = useState<string | null>('permanent');
const [options, setOptions] = useState(types);
const [chosen, setChosen] = useState<string[]>(['Python', 'Spark']);
const [record, setRecord] = useState<string | null>(null);
return (
<div className="grid w-full gap-300 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="editor-text">1 · Inline text</FieldLabel>
<Input id="editor-text" defaultValue="Senior Data Engineer" />
</Field>
<Field>
<FieldLabel htmlFor="editor-money">2 · Number and money</FieldLabel>
<Input id="editor-money" prefix="GBP" inputMode="decimal" align="end" defaultValue="148,500" />
</Field>
<Field>
<FieldLabel>3 · Select menu</FieldLabel>
<Picker
framed
label="Job type"
autoFocus={false}
items={options}
value={type}
onValueChange={(next) => setType(next as string | null)}
onCreate={(text) => {
setOptions([...options, { value: text, label: text, display: <Tag size="sm">{text}</Tag> }]);
setType(text);
}}
/>
</Field>
<Field>
<FieldLabel>4 · Multi-select</FieldLabel>
<Picker framed label="Skills" multiple autoFocus={false} items={skills} value={chosen} onValueChange={(next) => setChosen(next as string[])} />
</Field>
<Field>
<FieldLabel htmlFor="editor-date">5 · Date picker</FieldLabel>
<DatePicker id="editor-date" label="Start date" today="2026-10-16" defaultValue="2026-11-03" />
</Field>
<Field>
<FieldLabel>6 · Record and person picker</FieldLabel>
<Picker framed label="Company or person" autoFocus={false} items={records} value={record} onValueChange={(next) => setRecord(next as string | null)} onCreate={() => {}} createVerb="Create company" />
</Field>
</div>
);
}Empty, zero and extremes
Empty stays empty and zero is a value. Money never truncates and carries its currency code. Names in any script show exactly as stored; a long name ends in an ellipsis with the full name in a tooltip. An overdue date is in danger text.
- Empty
- Zero
- 0 VND
- Huge
- 25,000,000 VND
- Over 100%
- 124%
- Overdue
- 9 Oct
- Far future
- 1 Jan 2091
- Vietnamese
- Nguyễn Thị Minh Khai
- Chinese
- 王小明
- Apostrophe
- Zoë O’Brien-Smith
- Long name
- Halcyon Bank International Holdings and Trust Company plc
- One letter
- J
import { FieldDisplay, type FieldValue } from '@/ui/field-display';
const rows: [string, FieldValue | { type: FieldValue['type']; value: null }][] = [
['Empty', { type: 'text', value: null }],
['Zero', { type: 'currency', value: 0, currency: 'VND' }],
['Huge', { type: 'currency', value: 25_000_000, currency: 'VND' }],
['Over 100%', { type: 'percent', value: 124 }],
['Overdue', { type: 'date', value: '2026-10-09', overdue: true }],
['Far future', { type: 'date', value: '2091-01-01' }],
['Vietnamese', { type: 'person', value: { name: 'Nguyễn Thị Minh Khai' } }],
['Chinese', { type: 'person', value: { name: '王小明' } }],
['Apostrophe', { type: 'person', value: { name: 'Zoë O’Brien-Smith' } }],
['Long name', { type: 'record', value: { type: 'company', name: 'Halcyon Bank International Holdings and Trust Company plc' } }],
['One letter', { type: 'text', value: 'J' }],
];
// Display never prints NaN, null or Invalid Date: empty stays empty, zero is a value, money never truncates, names
// show exactly as stored, long text ends in an ellipsis with the full value in a tooltip.
export default function Extremes() {
return (
<dl className="grid w-full max-w-90 grid-cols-[7rem_minmax(0,1fr)] text-body leading-body">
{rows.map(([name, field]) => (
<div key={name} className="col-span-2 grid h-9 grid-cols-subgrid items-center border-b border-border">
<dt className="text-text-subtle">{name}</dt>
<dd className="min-w-0">
<FieldDisplay field={field} label={name} today="2026-10-16" />
</dd>
</div>
))}
</dl>
);
}Date time picker
A day and a start time as two fields side by side: the date picker and the time picker. Meetings use it, with the length beside it; times are in the agency's time zone.
Form
The parts every form is built from: field, label, messages, choice controls, larger inputs, and the settings layout that holds them.