Skip to content
Highlight

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

tsx
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.

tsx
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.

Label

Help text.

Label

Help text.

tsx
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.

Rolling buffer

A long note wraps into the column that has room, which in a settings row is the label’s.

tsx
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.

We only use this to find your team.

tsx
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.

Screen recording

Captures the active window.

Audio
Clipboard
tsx
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.

tsx
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.

PropTypeDefaultDescription
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.
requiredbooleanfalsePrints the asterisk on the label and marks the control required, from one flag.
invalidboolean—Forces the invalid state without waiting for validation.
disabledboolean—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.
namestring—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.