Skilllibrary forms-validation
install
source · Clone the upstream repo
git clone https://github.com/merceralex397-collab/skilllibrary
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/merceralex397-collab/skilllibrary "$T" && mkdir -p ~/.claude/skills && cp -r "$T/08-web-frontend-and-design/forms-validation" ~/.claude/skills/merceralex397-collab-skilllibrary-forms-validation && rm -rf "$T"
manifest:
08-web-frontend-and-design/forms-validation/SKILL.mdsource content
Purpose
Build accessible, validated forms using React Hook Form with Zod schema validation, proper error display, and submission handling.
When to use this skill
- building forms with client-side validation in React
- integrating Zod schemas with React Hook Form via
@hookform/resolvers - handling async submission with loading states and server errors
- making forms accessible (error announcements, label associations)
Do not use this skill when
- writing backend validation — different patterns apply
- managing global state without forms — prefer
state-management - the task is visual styling — prefer
tailwind-shadcn
Procedure
- Define Zod schema — create schema with
, add constraints:z.object()
,z.string().min(1).email()
.z.number().positive() - Set up React Hook Form —
.useForm({ resolver: zodResolver(schema), defaultValues }) - Build form fields — use
for uncontrolled orregister()
for controlled components. Always setController
andid
.htmlFor - Display errors — show
next to fields. UseformState.errors[field]?.message
linking error to input.aria-describedby - Handle submission — pass async handler to
. Set loading state, catch server errors, display in form.handleSubmit() - Add accessibility — associate
with inputs, announce errors with<label htmlFor>
, setaria-live="polite"
on errored fields.aria-invalid - Test — verify validation triggers on blur/submit, errors display and clear correctly, submission works with valid data.
Zod + React Hook Form
import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; const schema = z.object({ email: z.string().min(1, 'Required').email('Invalid email'), password: z.string().min(8, 'At least 8 characters'), age: z.coerce.number().min(18, 'Must be 18+').optional(), }); type FormData = z.infer<typeof schema>; function SignupForm() { const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({ resolver: zodResolver(schema), }); const onSubmit = async (data: FormData) => { await api.signup(data); }; return ( <form onSubmit={handleSubmit(onSubmit)} noValidate> <div> <label htmlFor="email">Email</label> <input id="email" type="email" aria-invalid={!!errors.email} aria-describedby={errors.email ? 'email-err' : undefined} {...register('email')} /> {errors.email && <p id="email-err" role="alert">{errors.email.message}</p>} </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" aria-invalid={!!errors.password} aria-describedby={errors.password ? 'pw-err' : undefined} {...register('password')} /> {errors.password && <p id="pw-err" role="alert">{errors.password.message}</p>} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Submitting...' : 'Sign Up'} </button> </form> ); }
Decision rules
- Use
onnoValidate
— let Zod handle validation, not the browser.<form> - Validate on blur for individual fields, on submit for the whole form —
.mode: 'onBlur' - Use
for numeric inputs — HTML inputs always return strings.z.coerce.number() - Share Zod schemas between client and server for consistent validation.
- Always associate errors with inputs via
andaria-describedby
.aria-invalid
References
Related skills
— typed component patternsreact-typescript
— form accessibility testingaccessibility-audit
— form state vs app state boundariesstate-management