Generic error messages at the top of a form — "Something went wrong" or "Invalid input" — require users to scan all fields to find what failed. WCAG 2.2 SC 3.3.1 (Error Identification) and SC 3.3.3 (Error Suggestion) mandate that validation errors identify the specific field and describe how to fix it, making inline field-level errors a legal accessibility requirement for many SaaS products. Beyond compliance, forms with global-only error banners have measurably higher abandonment rates — users who typed a complex password and can't tell which field is wrong often give up rather than re-enter everything.
High because field-level inline validation is a WCAG 2.2 requirement, and its absence directly increases form abandonment in authentication and onboarding flows.
Use react-hook-form with a Zod resolver to get per-field error messages with zero manual state management.
const schema = z.object({
email: z.string().email('Please enter a valid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
const { register, formState: { errors } } = useForm({ resolver: zodResolver(schema) })
// In JSX, render the error adjacent to the field:
<input {...register('email')} aria-invalid={!!errors.email} aria-describedby="email-error" />
{errors.email && <p id="email-error" className="text-destructive text-sm">{errors.email.message}</p>}
For server-side validation rejections, return field-keyed errors from the API route ({ error: 'Validation failed', fields: { email: 'Already registered' } }) and map them back to form state on the client using setError('email', { message: ... }).
ID: saas-error-handling.user-errors.form-validation-inline
Severity: high
What to look for: Examine form components throughout the codebase. Look for: (1) how validation errors are displayed — are they shown next to the specific field that failed, or in a single alert at the top/bottom of the form? (2) Are error messages specific ("Password must be at least 8 characters") or generic ("Invalid input")? (3) Are errors shown while the user is filling out the form (on blur or on change) or only after submit? Check for form libraries: react-hook-form, formik, Zod-based validation with react-hook-form, zod-form-data, valibot. Look for aria-describedby and aria-invalid attributes for accessibility. Check server-side validation responses — do they return field-specific errors or a single string?
Pass criteria: Count all form components in the project and examine each for inline validation. Pass if forms display validation errors adjacent to the specific field that failed AND error messages are specific enough to guide correction (not just "Invalid" or "Required"). At least 80% of forms must show field-level errors. Report the ratio: "X of Y forms display inline field-level validation errors."
Fail criteria: Fail if all validation errors are shown in a single toast or alert at the top of the form without identifying the specific field. Fail if error messages are generic and non-actionable ("Something is wrong", "Invalid value"). Do NOT pass when form validation exists but all errors display in a single global banner rather than adjacent to the failing field.
Skip (N/A) when: The project has no user-facing forms. Signal: no <form> elements, no form library dependencies, no form handler functions found in component files. This would be unusual for a SaaS application.
Detail on fail: "Login and signup forms use a single error state shown above the form; no per-field error display; error messages are generic strings". Max 500 chars.
Remediation: Generic or global validation errors require users to guess which field is wrong. Specific inline errors reduce friction and form abandonment.
With react-hook-form and Zod:
const schema = z.object({
email: z.string().email('Please enter a valid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
const { register, formState: { errors } } = useForm({ resolver: zodResolver(schema) })
// In JSX:
<input {...register('email')} aria-invalid={!!errors.email} />
{errors.email && <p className="text-destructive text-sm">{errors.email.message}</p>}
For server-side validation errors, return field-level error details:
return NextResponse.json({
error: 'Validation failed',
fields: { email: 'This email is already registered' }
}, { status: 422 })
Then map these back to form state on the client.
For a deeper analysis of your accessibility implementation including form labels and ARIA patterns, the Accessibility Fundamentals Audit covers this in detail.