ComponentsInputs and forms

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.

Permanent
Permanent, part time
Contract
Retained search
States
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
x
3 of 5
Date
3 Nov
Timestamp
2h ago, 16 Oct 2026, 12:05
Select
Permanent
Status
Interview
Tags
PythonSparkSQLAirflowdbt
Person
Joseph Ariel
Email
sara@halcyonbank.com
Phone
+44 20 7946 0958
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.

GBP
Permanent
Permanent, part time
Contract
Python
Scala
SQL
Spark
Airflow
Halcyon Bankhalcyonbank.com
Halden Labs
Hal Lindgren
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>
  );
}

On this page