Command Palette

Search for a command to run...

Form

Form is a component .

Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.

Well-designed HTML forms are:

  • Well-structured and semantically correct.
  • Easy to use and navigate (keyboard).
  • Accessible with ARIA attributes and proper labels.
  • Has support for client and server side validation.
  • Well-styled and consistent with the rest of the application.

In this guide, we will take a look at building forms with react-hook-form and zod. We're going to use a <FormField> component to compose accessible forms using Radix UI components.

Anatomy

<Form>
  <FormField
    control={...}
    name="..."
    render={() => (
      <FormItem>
        <FormLabel />
        <FormControl>
          { /* Your form field */}
        </FormControl>
        <FormDescription />
        <FormMessage />
      </FormItem>
    )}
  />
</Form>

Example

const form = useForm()
 
<FormField
  control={form.control}
  name="username"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Username</FormLabel>
      <FormControl>
        <Input placeholder="shadcn" {...field} />
      </FormControl>
      <FormDescription>This is your public display name.</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Installation

Follow these simple steps to add the File Trigger component to your project:

Install Dependencies

npm i @radix-ui/react-label @radix-ui/react-slot react-hook-form @hookform/resolvers zod

Create a new file components/ui/form.tsx and copy the code below:

"use client";
 
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import {
    Controller,
    FormProvider,
    useFormContext,
    type ControllerProps,
    type FieldPath,
    type FieldValues,
} from "react-hook-form";
 
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
import { HTMLMotionProps } from "motion/react";
 
const Form = FormProvider;
 
type FormFieldContextValue<
    TFieldValues extends FieldValues = FieldValues,
    TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
    name: TName;
};
 
const FormFieldContext = React.createContext<FormFieldContextValue>(
    {} as FormFieldContextValue,
);
 
const FormField = <
    TFieldValues extends FieldValues = FieldValues,
    TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
    ...props
}: ControllerProps<TFieldValues, TName>) => {
    return (
        <FormFieldContext.Provider value={{ name: props.name }}>
            <Controller {...props} />
        </FormFieldContext.Provider>
    );
};
 
const useFormField = () => {
    const fieldContext = React.useContext(FormFieldContext);
    const itemContext = React.useContext(FormItemContext);
    const { getFieldState, formState } = useFormContext();
 
    const fieldState = getFieldState(fieldContext.name, formState);
 
    if (!fieldContext) {
        throw new Error("useFormField should be used within <FormField>");
    }
 
    const { id } = itemContext;
 
    return {
        id,
        name: fieldContext.name,
        formItemId: `${id}-form-item`,
        formDescriptionId: `${id}-form-item-description`,
        formMessageId: `${id}-form-item-message`,
        ...fieldState,
    };
};
 
type FormItemContextValue = {
    id: string;
};
 
const FormItemContext = React.createContext<FormItemContextValue>(
    {} as FormItemContextValue,
);
 
const FormItem = React.forwardRef<
    HTMLDivElement,
    React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
    const id = React.useId();
 
    return (
        <FormItemContext.Provider value={{ id }}>
            <div ref={ref} className={cn("space-y-2", className)} {...props} />
        </FormItemContext.Provider>
    );
});
FormItem.displayName = "FormItem";
 
const FormLabel = React.forwardRef<HTMLLabelElement, HTMLMotionProps<"label">>(
    ({ className, ...props }, ref) => {
        const { error, formItemId } = useFormField();
 
        return (
            <Label
                ref={ref}
                className={cn(error && "text-destructive", className)}
                htmlFor={formItemId}
                {...props}
            />
        );
    },
);
FormLabel.displayName = "FormLabel";
 
const FormControl = React.forwardRef<
    React.ComponentRef<typeof Slot>,
    React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
    const { error, formItemId, formDescriptionId, formMessageId } =
        useFormField();
 
    return (
        <Slot
            ref={ref}
            id={formItemId}
            aria-describedby={
                !error
                    ? `${formDescriptionId}`
                    : `${formDescriptionId} ${formMessageId}`
            }
            aria-invalid={!!error}
            {...props}
        />
    );
});
FormControl.displayName = "FormControl";
 
const FormDescription = React.forwardRef<
    HTMLParagraphElement,
    React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
    const { formDescriptionId } = useFormField();
 
    return (
        <p
            ref={ref}
            id={formDescriptionId}
            className={cn("text-muted-foreground text-[0.8rem]", className)}
            {...props}
        />
    );
});
FormDescription.displayName = "FormDescription";
 
const FormMessage = React.forwardRef<
    HTMLParagraphElement,
    React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
    const { error, formMessageId } = useFormField();
    const body = error ? String(error?.message ?? "") : children;
 
    if (!body) {
        return null;
    }
 
    return (
        <p
            ref={ref}
            id={formMessageId}
            className={cn(
                "text-destructive text-[0.8rem] font-medium",
                className,
            )}
            {...props}
        >
            {body}
        </p>
    );
});
FormMessage.displayName = "FormMessage";
 
export {
    useFormField,
    Form,
    FormItem,
    FormLabel,
    FormControl,
    FormDescription,
    FormMessage,
    FormField,
};

Adjust the import paths in both files according to your project's structure.

This is your public display name.