Basel Standard

Basel Standard / Docs

Form

React Hook Form helper layer for accessible labels, descriptions, controls, and validation messages.

The form helpers connect the label, description, and error message to the same input automatically.

The Form helpers connect React Hook Form state to the visual primitives in this system. They keep ids, aria-describedby, invalid states, and message wiring consistent so each input does not need its own local accessibility bookkeeping.

Installation

Install
Purchase access, then open /account/install to issue a registry token.

Usage

import { useForm } from "react-hook-form"

import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
const form = useForm({
  defaultValues: { title: "" },
})

<Form {...form}>
  <FormField
    control={form.control}
    name="title"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Release title</FormLabel>
        <FormControl>
          <Input {...field} />
        </FormControl>
        <FormDescription>Name the review before sending it out.</FormDescription>
        <FormMessage />
      </FormItem>
    )}
  />
</Form>

Why This Primitive Exists

This layer does not replace React Hook Form. It narrows the repetitive glue around it.

  • Use it when a control needs consistent label, description, and error wiring.
  • Use it when different inputs should share the same accessibility contract without per-field ids.
  • Avoid it for static layout-only forms where Field is enough and no managed validation state is involved.

Examples

Basic Managed Field

The smallest useful pattern is one FormField mapped to one FormItem, with a label, control, helper text, and message output.

The form helpers connect the label, description, and error message to the same input automatically.

<FormField
  control={form.control}
  name="title"
  rules={{ required: "Enter the release title." }}
  render={({ field }) => (
    <FormItem>
      <FormLabel>Release title</FormLabel>
      <FormControl>
        <Input {...field} />
      </FormControl>
      <FormDescription>Use the internal release name.</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Composed Controls

FormControl works with wrapped inputs such as Select and checkbox layouts because it provides the shared ids and described-by chain through a slot.

Use the team or person who owns the operational signoff.

Wrapped controls still inherit the same ids and error wiring.

This suits compound controls where the check state, label, and supporting copy should still submit as one field.

<FormControl>
  <Select onValueChange={field.onChange} defaultValue={field.value}>
    <SelectTrigger>
      <SelectValue placeholder="Choose a cadence" />
    </SelectTrigger>
  </Select>
</FormControl>

In Context

The helpers stay useful when the form sits inside a larger card or review surface. Labels and messages remain attached even when the layout around them becomes more editorial.

Review workflow

Form helpers stay useful when the control is part of a larger decision surface.

Keep the title close to the workflow language the team already uses.

Descriptions remain attached to the text area and become part of the same accessibility chain.

<CardContent>
  <FormField
    control={form.control}
    name="note"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Release note</FormLabel>
        <FormControl>
          <Textarea rows={4} {...field} />
        </FormControl>
        <FormMessage />
      </FormItem>
    )}
  />
</CardContent>

Guidance

Keep Each FormField Narrow

  • Map one form value to one FormField.
  • Put complex layout around the field, not inside the render callback when possible.
  • If several controls belong to one visual group, use surrounding layout primitives and keep each control's accessibility chain intact.

Let FormControl Own The Wiring

  • Wrap the actual interactive element with FormControl.
  • Do not hand-roll id, aria-describedby, or invalid attributes unless a specific control requires it.
  • Keep FormLabel, FormDescription, and FormMessage inside the same FormItem so they resolve against the current field context.

Choose Field Vs Form Intentionally

  • Use Field when you need layout structure without form-library state.
  • Use Form when the control participates in validation, submission, or controlled state.
  • It is reasonable to combine both patterns, but avoid duplicating label or message responsibilities.

API Reference

Exports

ExportRendersNotes
FormFormProviderReact Hook Form provider alias.
FormFieldControllerBridges a named field into the context used by the helper components.
FormItemdivGenerates a stable id base for one field.
FormLabelLabelResolves htmlFor from the active field context.
FormControlSlotApplies id, aria-describedby, and aria-invalid to the child control.
FormDescriptionpHelper text tied to the current form field.
FormMessagepError or custom message tied to the current form field.
useFormFieldhookExposes the resolved ids, field name, and React Hook Form field state.

FormField

FormField accepts the standard ControllerProps<TFieldValues, TName> API from React Hook Form, including:

PropTypeNotes
controlRHF control objectRequired.
nameFieldPath<TFieldValues>Required field name.
rulescontroller rulesOptional validation rules.
renderrender callbackReceives { field, fieldState, formState }.

FormControl

FormControl forwards the slot props and automatically derives:

OutputBehavior
idSet to the generated form-item id.
aria-describedbyIncludes the description id and the message id when an error exists.
aria-invalidMirrors the active field error state.

Use these helpers inside FormField and FormItem. Outside that context, useFormField and the field-bound components will throw.