Accessible Form with Comprehensive Error Handling
Accessible form with proper labels, ARIA attributes, error handling, and screen reader support
// Accessible Form Component
import { useState, FormEvent } from 'react';
interface FormErrors {
[key: string]: string;
}
export function AccessibleForm() {
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrors({});
setIsSubmitting(true);
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
// Validation
const newErrors: FormErrors = {};
if (!email) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!password) {
newErrors.password = 'Password is required';
} else if (password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
setIsSubmitting(false);
return;
}
// Submit form
try {
await submitForm({ email, password });
} catch (error) {
setErrors({ form: 'An error occurred. Please try again.' });
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} noValidate>
{/* Email Input */}
<div>
<label htmlFor="email">
Email Address <span aria-label="required">*</span>
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
aria-errormessage={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<span
id="email-error"
role="alert"
aria-live="polite"
className="error"
>
{errors.email}
</span>
)}
</div>
{/* Password Input */}
<div>
<label htmlFor="password">
Password <span aria-label="required">*</span>
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
aria-required="true"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? 'password-error password-hint' : 'password-hint'}
aria-errormessage={errors.password ? 'password-error' : undefined}
/>
<span id="password-hint" className="hint">
Must be at least 8 characters
</span>
{errors.password && (
<span
id="password-error"
role="alert"
aria-live="polite"
className="error"
>
{errors.password}
</span>
)}
</div>
{/* Form-level errors */}
{errors.form && (
<div role="alert" aria-live="assertive" className="error">
{errors.form}
</div>
)}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}