Step by step on how to implement a modern Next.js form validation stack:
-
react-form-hookmanages the state, submission, and error tracking -
shadcnprovides the pre-styled accessible components (e.g. Card, Input, Field) that tie into the hook. -
zoddefines the "shape" of your data and validation rules
Demo example: https://mamolio.vercel.app/signin
Demo code: https://github.com/marwanzaky/mern-ecommerce
1. Install Dependencies
Go to your existing Next.js app directory and install react-form-hook and zod
npm install react-hook-form zod @hookform/resolvers
2. Install Shadcn Components
If you don't have shadcn installed already use this command
npx shadcn@latest init --preset b0 --template next
Then use this to add input, field, and card components
npx shadcn@latest add input
npx shadcn@latest add field
npx shadcn@latest add card
3. Login Form Page
Create new page app/signin/page.tsx and import card, field, button, input and zod components
// app/signin/page.tsx
"use client";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@shadcn/components/ui/card";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@components/ui/field";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Create sign up form schema
// app/signin/page.tsx
const SignUpSchema = z.object({
email: z
.email()
.nonempty("This field is required.")
.max(32, "Email is too short."),
password: z.string().nonempty("This field is required."),
});
type SignUpInput = z.infer<typeof SignUpSchema>;
Create react-form-hook
// app/signin/page.tsx
export default function Page() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignUpInput>({
resolver: zodResolver(SignUpSchema),
mode: "onSubmit",
defaultValues: {
email: "",
password: "",
},
});
const onSubmit = async (data: SignUpInput) => {
const { email, password } = data;
console.log("email", email);
console.log("password", password);
};
}
Example on how to use them to create fields
<Field data-invalid={!!errors.email}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<FieldContent>
<Input
id="email"
placeholder="m@example.com"
aria-invalid={!!errors.email}
{...register("email")}
/>
</FieldContent>
<FieldError>{errors.email?.message}</FieldError>
</Field>
Now let’s create card form with email and password fields and submit button
// app/signin/page.tsx
export default function Page() {
return (
<Card className="mx-auto w-full max-w-sm">
<CardHeader>
<CardTitle>Login to your account</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Field data-invalid={!!errors.email}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<FieldContent>
<Input
id="email"
placeholder="m@example.com"
aria-invalid={!!errors.email}
{...register("email")}
/>
</FieldContent>
<FieldError>{errors.email?.message}</FieldError>
</Field>
<Field data-invalid={!!errors.password}>
<div className="flex items-center">
<FieldLabel htmlFor="password">
Password
</FieldLabel>
<Link
href="/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<FieldContent>
<Input
id="password"
type="password"
aria-invalid={!!errors.password}
{...register("password")}
/>
</FieldContent>
<FieldError>{errors.password?.message}</FieldError>
</Field>
<Field>
<Button type="submit">Login</Button>
<Button type="button" variant="outline">
Login with Google
</Button>
<FieldDescription className="text-center">
Don't have an account?{" "}
<Link href="/signup">Sign up</Link>
</FieldDescription>
</Field>
</FieldGroup>
</form>
</CardContent>
</Card>
);
}
4. Why this stack works well
-
react-hook-form→ minimal re-renders, great performance -
zod→ schema = validation + types in one place -
shadcn→ accessible, composable UI components
You are all set to use form validation for anything really
More live examples
- Sign Up: https://mamolio.vercel.app/signup
- Contact Us: https://mamolio.vercel.app/contact
Feel free to review the code on GitHub
You may also want to learn How to Implement Google OAuth 2.0 in Next.js with NestJS


Top comments (0)