V2 componentsUpdated Oct 1, 2026
Field
Lays a label, a control and a note out together and wires them to each other. Built on Base UI Field, so htmlFor, aria-describedby and aria-invalid are generated rather than hand-passed.
Source: packages/ui/src/components/field
import { Field } from '@highlight/ui'Clicking the label focuses the control. required prints the asterisk and sets the attribute.
We only use this for meeting invites.
import { Field, Input } from '@highlight/ui' export default function FieldBasic() { return ( <div className="flex w-80 flex-col gap-5"> <Field> <Field.Label>Workspace name</Field.Label> <Input placeholder="Acme design…" /> </Field> <Field required> <Field.Label>Work email</Field.Label> <Input type="email" placeholder="you@acme.com" /> <Field.Note>We only use this for meeting invites.</Field.Note> </Field> </div> )}vertical stacks the label, control and note. horizontal puts the label in a fixed 120px lane and lets the control fill. justified lets the label fill and sizes the control to itself, for a settings row. leading puts a checkbox or radio first, on the label’s first line.
Help text.
Help text.
Help text.
Help text.
import { Checkbox, Field, Input, Select, type SelectItemType } from '@highlight/ui' const THEMES: Array<SelectItemType> = [ { label: 'System', value: 'system' }, { label: 'Light', value: 'light' }, { label: 'Dark', value: 'dark' },] export default function FieldOrientations() { return ( <div className="flex w-full max-w-md flex-col gap-8"> <Field> <Field.Label>Label</Field.Label> <Input /> <Field.Note>Help text.</Field.Note> </Field> <Field orientation="horizontal"> <Field.Label>Label</Field.Label> <Input /> <Field.Note>Help text.</Field.Note> </Field> <Field orientation="justified"> {/* A trigger is button-like, so the label is not a native one: it would hand the trigger its hover. */} <Field.Label nativeLabel={false}>Label</Field.Label> <Field.Note>Help text.</Field.Note> <Select items={THEMES} defaultValue="system"> <Select.Trigger /> <Select.Content> {THEMES.map((item) => ( <Select.Item key={String(item.value)} value={item.value}> {item.label} </Select.Item> ))} </Select.Content> </Select> </Field> <Field orientation="leading"> <Field.Label nativeLabel={false}>Label</Field.Label> <Checkbox defaultChecked> <Checkbox.Indicator /> </Checkbox> <Field.Note>Help text.</Field.Note> </Field> </div> )}The note sits under whichever side has room: the control in a form row, the label in a settings row. Under a label it is 2px away; under a control, 6px.
A long note wraps into the column that has room, which in a form row is the control’s.
A long note wraps into the column that has room, which in a settings row is the label’s.
import { Field, Input, Select, type SelectItemType } from '@highlight/ui' const BUFFER: Array<SelectItemType> = [ { label: 'Off', value: 'off' }, { label: '5 minutes', value: '5' }, { label: '15 minutes', value: '15' },] export default function FieldNotePlacement() { return ( <div className="flex w-full max-w-md flex-col gap-8"> <Field orientation="horizontal"> <Field.Label>Notification email</Field.Label> <Input type="email" /> <Field.Note>A long note wraps into the column that has room, which in a form row is the control’s.</Field.Note> </Field> <Field orientation="justified"> <Field.Label nativeLabel={false}>Rolling buffer</Field.Label> <Field.Note> A long note wraps into the column that has room, which in a settings row is the label’s. </Field.Note> <Select items={BUFFER} defaultValue="5"> <Select.Trigger /> <Select.Content> {BUFFER.map((item) => ( <Select.Item key={String(item.value)} value={item.value}> {item.label} </Select.Item> ))} </Select.Content> </Select> </Field> </div> )}Field.Group is a fieldset: disabled on it disables every control, and legend names the set. Fields sit 20px apart.
import { Field, Input } from '@highlight/ui' export default function FieldGroupExample() { return ( <Field.Group className="max-w-md"> <Field required> <Field.Label>Full name</Field.Label> <Input placeholder="Lin Fan" autoComplete="name" /> </Field> <Field required> <Field.Label>Work email</Field.Label> <Input type="email" placeholder="you@company.com" autoComplete="email" spellCheck={false} /> <Field.Note>We only use this to find your team.</Field.Note> </Field> <Field> <Field.Label>Job title</Field.Label> <Input placeholder="Head of Design" autoComplete="organization-title" /> </Field> </Field.Group> )}orientation on the group sets every field to that shape and tightens the spacing to match: 16px for settings rows, 12px for checkbox rows.
import { Field, Select, type SelectItemType } from '@highlight/ui' const CAPTURE: Array<SelectItemType> = [ { label: 'On', value: 'on' }, { label: 'Off', value: 'off' },] function CaptureSelect({ defaultValue }: { defaultValue: string }) { return ( <Select items={CAPTURE} defaultValue={defaultValue}> <Select.Trigger /> <Select.Content> {CAPTURE.map((item) => ( <Select.Item key={String(item.value)} value={item.value}> {item.label} </Select.Item> ))} </Select.Content> </Select> )} export default function FieldSettingsList() { return ( <Field.Group orientation="justified" className="max-w-md"> <Field> <Field.Label nativeLabel={false}>Screen recording</Field.Label> <Field.Note>Captures the active window.</Field.Note> <CaptureSelect defaultValue="on" /> </Field> <Field> <Field.Label nativeLabel={false}>Audio</Field.Label> <CaptureSelect defaultValue="on" /> </Field> <Field> <Field.Label nativeLabel={false}>Clipboard</Field.Label> <CaptureSelect defaultValue="off" /> </Field> </Field.Group> )}Note match narrows an error to one ValidityState key, so a field answers each way of being wrong with its own sentence.
Contact support to change this.
import { Field, Input } from '@highlight/ui' export default function FieldValidation() { return ( <div className="flex w-80 flex-col gap-5"> <Field required validationMode="onBlur"> <Field.Label>Work email</Field.Label> <Input type="email" required placeholder="you@acme.com" /> <Field.Note error match="valueMissing"> We need an email to send invites to. </Field.Note> <Field.Note error match="typeMismatch"> That is not a full email address. </Field.Note> </Field> <Field disabled> <Field.Label>Workspace URL</Field.Label> <Input defaultValue="acme.highlight.ing" /> <Field.Note>Contact support to change this.</Field.Note> </Field> </div> )}The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.
Field- Base UI Field.Root. Lays out label, control and note by orientation and wires their ids; required and disabled reach the control.
Field.Label- A real <label>, wired to the control and placed as a sibling of it. srOnly, nativeLabel.
Field.Group- A fieldset of fields. orientation sets each field’s shape and the gap; legend titles the set.
Field.Note- Help text at rest; error renders it as the field error, narrowed by match.
Field.Item- One option in a RadioGroup or checkbox group: control, Label and Note laid out like a leading Field. disabled greys the whole option.
Field.Control / Validity- Base UI parts, for what the wrapper does not cover.
Props added on top of the underlying element or Base UI part.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'vertical'|'horizontal'|'justified'|'leading' | 'vertical' | Form column, form row with a label lane, a settings row with the control at the far end, or a checkbox row with the control first. Defaults to the enclosing Field.Group’s. |
required | boolean | false | Prints the asterisk on the label and marks the control required, from one flag. |
invalid | boolean | — | Forces the invalid state without waiting for validation. |
disabled | boolean | — | Disables the control; the label and note take the disabled colour with it. |
validationMode | 'onSubmit'|'onBlur'|'onChange' | 'onSubmit' | When the control revalidates. onChange takes validationDebounceTime. |
validate | (value, formValues) => string | string[] | null | — | Custom validation. |
name | string | — | Field name, used by Base UI Form for validation and submission. |
- One structure everywhere: Field.Label, the control, then Field.Note, as siblings. The orientation decides where each lands.
- Any control that reads Base UI Field picks the wiring up by sitting inside: Input, Select.Trigger, Checkbox.
- Pass nativeLabel={false} on Label when the control is button-like. A native label hands its hover to the control, so a Select.Trigger would light up whenever the pointer crossed the words.
- The label lane is a fixed 120px, not max-content. A lane that sizes to its content steps the left edge of every row in a list, so override the width on the form rather than on one field.