Step 8
Step 8 — Forms, validation, UX
25 min
Step 8 — Forms, validation, UX
Beyond pretty markup — real frontend is kindly handling user mistakes.
Validate input with zod
import { z } from "zod";
const SignupSchema = z.object({
email: z.string().email("Not a valid email"),
password: z.string().min(8, "At least 8 characters"),
age: z.number().int().min(14, "14 or older"),
});
const result = SignupSchema.safeParse(formData);
if (!result.success) {
// result.error.issues lists the failed fields
}
One pattern, all your forms.
Loading UX — those 400ms matter
If the screen freezes during a request, the user thinks it's broken. Three habits:
- Disable button + change label —
<button disabled>{loading ? "Sending…" : "Send"}</button> - Skeleton UI — gray placeholder with
animate-pulse - Optimistic update — show the result first, roll back on failure
const [loading, setLoading] = useState(false);
async function onSubmit() {
setLoading(true);
try { await fetch("/api/signup", { method: "POST", body: ... }); }
finally { setLoading(false); }
}
Accessibility one-liner
Every <input> needs a <label>. Every <button> text should describe the action.
<label for="email">Email</label>
<input id="email" type="email" required>
Screen-reader users need to know which field they're filling.
Try it
Extend the Counter with three buttons (increment / decrement / reset). Add aria-label to each, and disable decrement when n is 0.
Next
That's the course. Continue with the nextjs-fullstack course (DB-connected Next.js) or backend-with-spring (real backend).
🎉 You finished From HTML/CSS/JS to React, Next.js, Tailwind
What's next? Pick another course below.