Docs
Form
Form
A Form component built with React Hook Form and Zod
Features
The <Form /> component is a wrapper around react-hook-form and provides us with a few helpful things:
- A
<FormField />component for building controlled form fields. - Form validation using
zod. - Handles accessibility and error messages.
- Applies the correct
ariaaccessibility attributes to form fields based on states. - Most importantly for Unstlyed, You have full control over the markup and styling.
Structure
<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="name"
render={({ field }) => (
<FormItem>
<FormLabel>Project Name</FormLabel>
<FormControl>
<Input placeholder="Unstyled" {...field} />
</FormControl>
<FormDescription>This is the name of your project.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
Installation
Install the following dependencies:
npm install react-hook-form @hookform/resolvers zod
Copy and paste the following code into your component
import * as React from "react";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { Label } from "@/components/ui/label/label";
import styles from "./form.module.css";
const Form = FormProvider;
type FormFieldContextValue<
_FieldValues extends FieldValues = FieldValues,
_Name extends FieldPath<_FieldValues> = FieldPath<_FieldValues>
> = {
name: _Name;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
const FormField = <
_FieldValues extends FieldValues = FieldValues,
_Name extends FieldPath<_FieldValues> = FieldPath<_FieldValues>
>({
...props
}: ControllerProps<_FieldValues, _Name>) => {
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 must be used in a <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
/* -------------------------------------------------------------------------------------------------
* FORM ITEM
* -----------------------------------------------------------------------------------------------*/
const ITEM_NAME = "FormItem";
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={`${styles.formItem} ${className ?? ""}`}
{...props}
/>
</FormItemContext.Provider>
);
});
FormItem.displayName = ITEM_NAME;
/* -------------------------------------------------------------------------------------------------
* FORM LABEL
* -----------------------------------------------------------------------------------------------*/
const LABEL_NAME = "FormLabel";
type LabelProps = React.HTMLProps<HTMLLabelElement> & {
className?: string;
};
const FormLabel: React.FC<LabelProps> = ({ className, ...props }) => {
const { error, formItemId } = useFormField();
return (
<Label
className={`${error && styles.error} ${className ?? ""}`}
htmlFor={formItemId}
{...props}
/>
);
};
FormLabel.displayName = LABEL_NAME;
/* -------------------------------------------------------------------------------------------------
* FORM CONTROL
* -----------------------------------------------------------------------------------------------*/
const CONTROL_NAME = "FormControl";
const FormControl = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<div
ref={ref}
id={formItemId}
className={styles.formItem}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = CONTROL_NAME;
/* -------------------------------------------------------------------------------------------------
* FORM DESCRIPTION
* -----------------------------------------------------------------------------------------------*/
const DESCRIPTION_NAME = "FormDescription";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={`${styles.description} ${className ?? className}`}
{...props}
/>
);
});
FormDescription.displayName = DESCRIPTION_NAME;
/* -------------------------------------------------------------------------------------------------
* FORM MESSAGE
* -----------------------------------------------------------------------------------------------*/
const MESSAGE_NAME = "FormMessage";
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={`${styles.message} ${className ?? ""}`}
{...props}>
{body}
</p>
);
});
FormMessage.displayName = MESSAGE_NAME;
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};
Copy and paste the following code into a .module.css file.
.error {
color: hsl(var(--destructive));
}
.description {
color: hsl(var(--muted-foreground));
font-size: var(--text-sm);
font-weight: var(--semiBold);
}
.message {
color: hsl(var(--destructive));
font-size: var(--text-sm);
font-weight: var(--semiBold);
}
.formItem {
margin-block: 0.4rem;
}
Update the import paths to match your project setup.
Usage
Define your form schema
Define the shape of your form using a Zod schema.
"use client";
import { z } from "zod";
const formSchema = z.object({
name: z.string().min(2).max(50),
});
Define your form
Use the useForm hook from react-hook-form to create a form.
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
const formSchema = z.object({
name: z.string().min(2, {
message: "Project Name must be at least 2 characters.",
}),
});
export function ProjectForm() {
// Create your form
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
},
});
// Create your submit handler
function onSubmit(values: z.infer<typeof formSchema>) {
// All your values will be typesafe and validated!
}
}
Since FormField is using a controlled component, you'll need to provide a default value for the field.
Build your form
Now we can use the <Form /> components to build our form.
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const formSchema = z.object({
name: z.string().min(2, {
message: "Project Name must be at least 2 characters.",
}),
});
export function ProjectForm() {
// ... your useForm hook and submit handler
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>Project Name</FormLabel>
<FormControl>
<Input placeholder='Unstyled' {...field} />
</FormControl>
<FormDescription>
This is the name of your project name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type='submit'>Submit</Button>
</form>
</Form>
);
}
Finished
That's it! You now have an accessible form that is fully type-safe and comes with client-side validation.