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/formNo 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.
Examples
Contact form
Submit it empty to see the errors. Labels, descriptions and errors are wired to the controls for you.
API
| Prop | Type | Default |
|---|---|---|
control, name, rulesPassed to Controller. | Controller props | — |
labelRendered as a FieldLabel for the control. | ReactNode | — |
descriptionHelp text under the control. | ReactNode | — |
orientationHorizontal for checkboxes and switches. | "vertical" | "horizontal" | "vertical" |
renderRender 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 };