Pho Design System

Field

The accessible scaffolding around a single form control — label, description, and validation error, all wired together. Built on Base UI Field. Input already composes Field internally; reach for Field directly when you need to wrap a custom control or lay out the pieces yourself.

Import

import { Field } from "@photon-ai/pho-ui/components/field";

Basic

Field.Label is associated with Field.Control automatically — no manual htmlFor. Field.Description adds helper text that stays put.

Used in your project URLs. Lowercase letters and dashes only.

<Field.Root>
  <Field.Label>Workspace name</Field.Label>
  <Field.Control placeholder="acme-inc" />
  <Field.Description>Lowercase letters and dashes only.</Field.Description>
</Field.Root>

Validation

Field.Error appears when the control fails validation. Set validationMode on Field.Root to choose when that runs (onBlur here), and give each Field.Error a match to write copy per failure reason — otherwise it shows the browser’s native message. The control picks up the invalid ring automatically via data-invalid.

We'll send the invite here.

<Field.Root validationMode="onBlur">
  <Field.Label>Work email</Field.Label>
  <Field.Control type="email" required placeholder="[email protected]" />
  <Field.Description>We'll send the invite here.</Field.Description>
  <Field.Error match="valueMissing">Enter your work email.</Field.Error>
  <Field.Error match="typeMismatch">
    That doesn't look like an email address.
  </Field.Error>
</Field.Root>

Several controls

One label, several controls: the lines of an address, first and last name, the parts of a date. Field.Controls stacks them at the gap the system uses between the parts of one thing, and the description lands under the whole block, not the first line. Put BasicPage.Inline inside for a side-by-side row.

The registry doesn't accept PO boxes.

<Field.Root>
  <Field.Label>Address</Field.Label>
  <Field.Controls>
    <Input aria-label="Street address" placeholder="123 Main St" />
    <Input
      aria-label="Suite, floor, or unit"
      placeholder="Suite, floor, or unit (optional)"
    />
    <BasicPage.Inline>
      <Input aria-label="City" placeholder="City" />
      <Input aria-label="ZIP code" placeholder="ZIP" />
    </BasicPage.Inline>
  </Field.Controls>
  <Field.Description>The registry doesn't accept PO boxes.</Field.Description>
</Field.Root>

Anatomy

Props

All parts forward className and their native element attributes.

Accessibility

Best practices