ToroUI

Forms

Building forms with Toro UI's field system and validation.


Toro UI provides a composable field system for building forms. Pair it with Zod for schema validation and you get type-safe forms with accessible error handling.

Field Components

The field system is made up of composable parts:

ComponentPurpose
FieldSetGroups related fields, renders <fieldset>
FieldLegendTitle for a field set, renders <legend>
FieldGroupLayout container for a group of fields
FieldWraps a single form control with its label and messages
FieldLabelAccessible label for a form control
FieldContentWrapper for description and error below a control
FieldDescriptionHelp text for a field
FieldErrorValidation error message
FieldSeparatorVisual divider between fields

Basic Form

import { Input } from "@/components/ui/input"
import {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldSet,
  FieldLegend,
} from "@/components/ui/field"

<form>
  <FieldSet>
    <FieldLegend>Contact Information</FieldLegend>
    <FieldGroup>
      <Field>
        <FieldLabel>Name</FieldLabel>
        <Input placeholder="Your name" />
      </Field>
      <Field>
        <FieldLabel>Email</FieldLabel>
        <Input type="email" placeholder="you@example.com" />
        <FieldDescription>We'll never share your email.</FieldDescription>
      </Field>
    </FieldGroup>
  </FieldSet>
</form>

Field Orientation

Fields support three orientation modes for label placement:

// Label above the input (default)
<Field orientation="vertical">
  <FieldLabel>Name</FieldLabel>
  <Input />
</Field>

// Label beside the input
<Field orientation="horizontal">
  <FieldLabel>Name</FieldLabel>
  <Input />
</Field>

// Vertical on mobile, horizontal on wider screens
<Field orientation="responsive">
  <FieldLabel>Name</FieldLabel>
  <Input />
</Field>

Validation with Zod

Use Zod schemas for type-safe validation. The FieldError component can display a list of error messages:

import { z } from "zod"

const schema = z.object({
  email: z.string().email("Please enter a valid email"),
  password: z.string().min(8, "Password must be at least 8 characters"),
})

function LoginForm() {
  const [errors, setErrors] = useState({})

  function onSubmit(e) {
    e.preventDefault()
    const data = Object.fromEntries(new FormData(e.target))
    const result = schema.safeParse(data)

    if (!result.success) {
      setErrors(result.error.flatten().fieldErrors)
      return
    }

    // Handle valid data
  }

  return (
    <form onSubmit={onSubmit}>
      <FieldGroup>
        <Field data-invalid={!!errors.email}>
          <FieldLabel>Email</FieldLabel>
          <Input name="email" type="email" aria-invalid={!!errors.email} />
          <FieldError errors={errors.email?.map(m => ({ message: m }))} />
        </Field>
        <Field data-invalid={!!errors.password}>
          <FieldLabel>Password</FieldLabel>
          <Input name="password" type="password" aria-invalid={!!errors.password} />
          <FieldError errors={errors.password?.map(m => ({ message: m }))} />
        </Field>
      </FieldGroup>
    </form>
  )
}

Error Display

FieldError handles single and multiple errors automatically:

// Single error — renders as text
<FieldError errors={[{ message: "Required" }]} />

// Multiple errors — renders as a list
<FieldError errors={[
  { message: "Must be at least 8 characters" },
  { message: "Must contain a number" },
]} />

// Or pass children directly
<FieldError>This field is required</FieldError>

Checkbox and Radio Groups

Use Field with checkbox and radio components for consistent layout:

<FieldSet>
  <FieldLegend variant="label">Notifications</FieldLegend>
  <Field orientation="horizontal">
    <Checkbox id="email-notifications" />
    <FieldLabel htmlFor="email-notifications">
      Email notifications
    </FieldLabel>
  </Field>
  <Field orientation="horizontal">
    <Checkbox id="sms-notifications" />
    <FieldLabel htmlFor="sms-notifications">
      SMS notifications
    </FieldLabel>
  </Field>
</FieldSet>

Field Separators

Use FieldSeparator to visually divide groups of fields, optionally with a label:

<FieldGroup>
  <Field>
    <FieldLabel>Email</FieldLabel>
    <Input type="email" />
  </Field>
  <FieldSeparator>or</FieldSeparator>
  <Field>
    <FieldLabel>Phone</FieldLabel>
    <Input type="tel" />
  </Field>
</FieldGroup>