4. Forms
Assignment Links
- Starter Code
- Finished Solution (what you will build)
- Lecture 5: Forms & Validation
- Lecture 6: Basic CSS & Tailwind
- GitHub Classroom Assignment
Assignment Overview
You are a front-end developer for a hot new ed-tech startup. You are tasked with creating a sign up form for new users to create accounts on the platform. You need to make sure that the form is user-friendly, responsive, and most importantly, validates user input before sending it to the backend.
First, take a look at the finished solution to see what you will be building. Try filling in the form with both valid and invalid data to see how the form behaves. Each individual field has its own rules, and error messages explain what went wrong when a user tries to submit invalid data.
You will start by defining a zod validation schema to define the shape of the form data and the validation rules for each field. Then we will integrate it with React Hook Form to handle the form state, and associate inputs with fields in our schema. Finally, we will show how to use pre-built shadcn components with React Hook Form to create a more polished and professional look.
You will only have to edit validator.ts, Form.tsx, and ControlledForm.tsx in parts 1-3 respectively.
Tailwind CSS Overview & Integration
What is Tailwind CSS?
Tailwind CSS is a utility-first CSS framework that lets you style your UI by composing small, reusable utility classes. Instead of writing custom CSS for each component, you can apply classes directly in your JSX to manage layout, spacing, colors, and more.
Commonly Used Utility Classes:
-
Layout:
container,mx-auto(center content)flex,grid(flexbox and grid layouts)
-
Spacing:
- Padding:
p-4,pt-2,px-6 - Margin:
m-4,mt-2,mb-6
- Padding:
-
Typography:
- Text size & weight:
text-lg,font-bold,text-center
- Text size & weight:
-
Colors:
- Background:
bg-blue-500,bg-gray-100 - Text:
text-white,text-gray-800 - Border:
border,border-gray-300
- Background:
-
Effects & States:
- Shadows:
shadow,shadow-lg - Rounded corners:
rounded,rounded-md - Hover effects:
hover:bg-blue-600,hover:text-white
- Shadows:
-
Responsive Design:
- Prefix with breakpoints:
sm:,md:,lg:, etc.
- Prefix with breakpoints:
Incorporating Tailwind into the Forms Project:
-
Verify Installation:
Tailwind CSS is already part of the project dependencies. Confirm that yourpackage.jsonincludes Tailwind and that atailwind.config.tsfile exists. -
Include Tailwind Directives:
In your main CSS file (for example,src/index.css), add the following at the top:@tailwind base;
@tailwind components;
@tailwind utilities;This ensures that Tailwind’s base styles, component styles, and utility classes are available throughout the project.
-
Customize the Config:
Opentailwind.config.tsand update or extend the theme as needed. For example, you might add custom colors or spacing that match your design:extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
},
},This lets you use classes like
bg-backgroundandtext-foreground. -
Using Utility Classes in JSX:
You can now apply Tailwind classes directly in your components. For instance, to add padding and a background color:<div className="p-4 bg-gray-100">
{/* form content */}
</div>Feel free to mix Tailwind classes with shadcn components for custom styling.
-
Restart the Dev Server:
After making changes, run:bun devand view your project at http://localhost:5173/.
Tailwind CSS Practical Exercises
Now that Tailwind CSS is integrated into the project, try these exercises to get hands-on practice:
-
Form Container Styling:
- Add utility classes to the form container (e.g. in
Form.tsx) to center it on the page usingmx-autoand add padding withp-6orp-4. - Experiment with background colors like
bg-gray-100or your custombg-background.
- Add utility classes to the form container (e.g. in
-
Input Field Styling:
- Style the input fields by adding classes such as
border,rounded, andp-2for spacing. - Add focus states with classes like
focus:outline-noneandfocus:ring-2to improve usability.
- Style the input fields by adding classes such as
-
Button Styling:
- Enhance the submit button by applying classes such as:
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded" - This will give the button a modern look and a responsive hover effect.
- Enhance the submit button by applying classes such as:
-
Responsive Design:
- Use responsive utility classes to adjust layout on different screen sizes. For instance, change padding or margins on mobile vs. desktop with classes like
p-4 md:p-8.
- Use responsive utility classes to adjust layout on different screen sizes. For instance, change padding or margins on mobile vs. desktop with classes like
-
Custom Color Usage:
- Update your
tailwind.config.tswith custom colors if you haven't already. - Apply these custom colors in your components by using classes like
bg-backgroundandtext-foreground.
- Update your
Take a few minutes to experiment with these exercises. They’re designed to give you a practical feel for how Tailwind CSS can rapidly improve your UI design.
Setup
Install the dependencies:
bun install
Start the development server:
bun dev
Then view the starter code at http://localhost:5173/
Part 1: Form Content and Validation
We want our form to have the following fields and rules. Open up validator.ts in src/lib/ and add each field to the zod validator. Remember that zod fields are required by default.
-
firstName: A required, nonempty string
-
lastName: A required, nonempty string
-
email: A required email address. Zod has a built in
.email()method that you can use to validate email addresses. -
role: An optional field that must be one of "student", "educator", or "parent/guardian". Use a zod enum to ensure that the role can't just be any arbitrary string.
-
subscribe: A required boolean field
-
birthDate: An optional field that must be a valid date in the past. You can use the
.date()method from zod to validate this. -
password: A required field that must pass the following rules
- Between 8-20 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
These properties can be enforced through regular expressions (regex), which zod supports with the.regex()operator. Useful regex patterns include: - Uppercase letter:
/[A-Z]/ - Lowercase letter:
/[a-z]/ - Digit:
/[\d]/
Use.min()and.max()to enforce length.
-
confirmPassword: A required field that must match the password field. Use the
.refine()method to validate this field in context with the other fields in the form. When refining, specify that the error exists on theconfirmPasswordpath.
Native HTML date inputs return a string, so instead of using z.date(), use z.coerce.date() to convert and validate.
Ensure that required string fields do not pass validation if empty. Use .min(1) to enforce a minimum length.
Test your schema in the zod playground if needed.
Part 2: HTML Form
Open Form.tsx located in src/components/Form.tsx. This file contains the form that you will be working with. All imports are already written for you.
Part 2.1: Form Setup
A form is declared for you using the useForm hook from React Hook Form. This hook returns functions and properties that help manage form state. We have destructured register, handleSubmit, and errors:
- Destructured
- Not Destructured
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Inputs>({
resolver: zodResolver(schema),
});
const form = useForm<Inputs>({
resolver: zodResolver(schema),
});
The zodResolver ensures that our form uses our zod schema for validation. Define the Inputs type using z.infer based on your schema:
const mySchema = z.object({
name: z.string(),
age: z.number(),
});
type Inputs = z.infer<typeof mySchema>;
Part 2.2: Registering Form Fields
We already have an input for the first name field. Use the register function to bind it to the firstName field in your schema:
<input {...register("firstName")} id="firstName" type="text" />
Create inputs for the other fields and register them accordingly.
Hint: password and checkbox inputs
Pass type="password" and type="checkbox" to create password and checkbox inputs.
Hint: role select
For dropdowns, use a select element:
<select {...register("role")} id="role">
<option value="student">Student</option>
<option value="educator">Educator</option>
<option value="parent/guardian">Parent/Guardian</option>
</select>
Test your form by filling out the fields and submitting. The button with type="submit" triggers the onSubmit handler.
Part 2.3: Error Messages
Display error messages under each input field using the errors object from React Hook Form:
{errors.firstName && (
<p className="text-red-500">{errors.firstName.message}</p>
)}
If there’s no error, the message won’t display. Customize the error messages as needed.
Part 3: Controlled Form with Shadcn Components
After deploying the form, your boss asks for a more professional design. Instead of building everything from scratch, you’ll use shadcn components.
Run the following command to add the shadcn input component:
bunx --bun shadcn@latest add input
This command creates a new file in src/components/ui/input.tsx.
Using the Components
Open ControlledForm.tsx in src/components/ControlledForm.tsx and uncomment the Input component import. Shadcn components handle form errors and labels out of the box. Use React Hook Form’s control object to make them controlled components.
For example:
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address *</FormLabel>
<FormControl>
<Input placeholder="example@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Refer to the shadcn docs for additional examples. For the date picker, copy the controlled date picker example.
Using shadcn improves accessibility by adhering to ARIA standards, making your form accessible for users with disabilities.
Optional Bonus: Forms with Arrays and Conditional Fields
Often, forms need to handle arrays of data. For example, allowing a parent/guardian to add multiple children to their account.
Updating the Schema
Add an array of children objects to the zod schema. For example:
export const formSchema = z.object({
// Existing fields...
children: z
.object({
name: z.string().min(1, "Required"),
grade: z.number().min(1).max(12),
})
.array()
.optional(),
});
useFieldArray()
Declare the useFieldArray() hook:
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "children",
});
Render each child with mapped inputs:
{
fields.map((child, index) => (
<div key={child.id} className="flex flex-col space-y-2">
<FormField
control={form.control}
name={`children.${index}.name`}
render={({ field }) => (
<FormItem>
<FormLabel>Child {index + 1} Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
variant={"ghost"}
onClick={() => remove(index)}
className="self-end"
>
<TrashIcon />
</Button>
</div>
));
}
Remember:
- Use the index in the array for property access (e.g.,
children.0.name). - Use the unique
idfromuseFieldArray()as the key.
Conditionally render the children fields only if role is "parent/guardian":
{form.watch("role") === "parent/guardian" && (
// Render children inputs here
)}
Congrats! You now have a fully functional sign up form that uses both shadcn components and Tailwind CSS to create a beautiful, responsive UI. Happy coding!