Skip to content
Corsair UI
Browse the docs

ComponentsNo. 28 of 109

Form Field (react-hook-form)

FormField: a react-hook-form Controller laid out with Field, with the id, label and ARIA wiring done for you.

Installation

pnpm dlx shadcn@latest add @corsair-ui/form

No setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.

Also adds
utilslabelfield
npm
@radix-ui/react-labelclass-variance-authorityclsxreact-hook-formtailwind-merge

Examples

Contact form

Submit it empty to see the errors. Labels, descriptions and errors are wired to the controls for you.

Only used to reply to you.

API

PropTypeDefault
control, name, rules

Passed to Controller.

Controller props—
label

Rendered as a FieldLabel for the control.

ReactNode—
description

Help text under the control.

ReactNode—
orientation

Horizontal for checkboxes and switches.

"vertical" | "horizontal""vertical"
render

Render the control; field carries id, aria-invalid and aria-describedby.

({ field, fieldState }) => ReactElement—

Built on React Hook Form; every prop it takes is passed through.

Accessibility

  • Generates the id, points the label at it and wires aria-invalid and aria-describedby.

Compatibility

Tailwind CSS
3.4 and 4, both checked in CI
React
19
Rendering
Client component ("use client")

Source

1 file, installed as source you own
form.tsxView on GitHub
"use client";

import { useId, type ReactElement, type ReactNode } from "react";
import {
  Controller,
  type ControllerFieldState,
  type ControllerProps,
  type ControllerRenderProps,
  type FieldPath,
  type FieldValues,
} from "react-hook-form";

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/registry/default/ui/field";

type FormControlProps<
  TFieldValues extends FieldValues,
  TName extends FieldPath<TFieldValues>,
> = ControllerRenderProps<TFieldValues, TName> & {
  id: string;
  "aria-invalid": boolean;
  "aria-describedby"?: string;
};

interface FormFieldProps<
  TFieldValues extends FieldValues,
  TName extends FieldPath<TFieldValues>,
> extends Omit<ControllerProps<TFieldValues, TName>, "render"> {
  label?: ReactNode;
  description?: ReactNode;
  /** Horizontal puts the control before its label (checkboxes, switches). */
  orientation?: "vertical" | "horizontal";
  className?: string;
  /**
   * Render the control. Spread `field` on inputs; for Checkbox, Switch or
   * Select map `field.value` / `field.onChange` to their own props.
   */
  render: (props: {
    field: FormControlProps<TFieldValues, TName>;
    fieldState: ControllerFieldState;
  }) => ReactElement;
}

/**
 * A react-hook-form field laid out with Field. It generates the id, points the
 * label at it, and sets aria-invalid and aria-describedby on the control so
 * the description and error are announced with it.
 *
 * @example
 * <FormField
 *   control={form.control}
 *   name="email"
 *   label="Email"
 *   description="We only use it to reply to you."
 *   render={({ field }) => <Input type="email" {...field} />}
 * />
 */
function FormField<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  label,
  description,
  orientation = "vertical",
  className,
  render,
  ...controller
}: FormFieldProps<TFieldValues, TName>) {
  const id = useId();
  const descriptionId = `${id}-description`;
  const errorId = `${id}-error`;

  return (
    <Controller
      {...controller}
      render={({ field, fieldState }) => {
        const describedBy =
          [description ? descriptionId : null, fieldState.invalid ? errorId : null]
            .filter(Boolean)
            .join(" ") || undefined;

        const control = render({
          field: {
            ...field,
            id,
            "aria-invalid": fieldState.invalid,
            "aria-describedby": describedBy,
          },
          fieldState,
        });
        const labelNode = label ? <FieldLabel htmlFor={id}>{label}</FieldLabel> : null;
        const descriptionNode = description ? (
          <FieldDescription id={descriptionId}>{description}</FieldDescription>
        ) : null;
        const errorNode = <FieldError id={errorId} errors={[fieldState.error]} />;

        return (
          <Field
            orientation={orientation}
            data-invalid={fieldState.invalid || undefined}
            data-disabled={field.disabled || undefined}
            className={className}
          >
            {orientation === "horizontal" ? (
              <>
                {control}
                <FieldContent>
                  {labelNode}
                  {descriptionNode}
                  {errorNode}
                </FieldContent>
              </>
            ) : (
              <>
                {labelNode}
                {control}
                {descriptionNode}
                {errorNode}
              </>
            )}
          </Field>
        );
      }}
    />
  );
}

export { FormField, type FormControlProps, type FormFieldProps };