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
// 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";
const formSchema = z.object({
email: z
.email()
.max(32, "Email is too short.")
.nonempty("This field is required."),
password: z.string().nonempty("This field is required."),
});
type FormValues = z.infer<typeof formSchema>;
export default function LoginForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
mode: "onChange",
defaultValues: {
email: "",
password: "",
},
});
const onSubmit = async (data: FormValues) => {
const { email, password } = data;
console.log("email", email);
console.log("password", password);
};
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>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="email">
Email
</FieldLabel>
<Input
{...field}
id="email"
aria-invalid={fieldState.invalid}
placeholder="m@example.com"
autoComplete="off"
/>
{fieldState.invalid && (
<FieldError
errors={[fieldState.error]}
/>
)}
</Field>
)}
/>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<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>
<Input
{...field}
id="password"
type="password"
aria-invalid={fieldState.invalid}
autoComplete="off"
/>
{fieldState.invalid && (
<FieldError
errors={[fieldState.error]}
/>
)}
</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)