Accessibility Guide
Learn how to create accessible forms with Hero Hook Form that work for all users.
ARIA Support
Automatic ARIA Attributes
Hero Hook Form automatically adds proper ARIA attributes to all field components:
// Input field with automatic ARIA support
<InputField
name="email"
label="Email Address"
description="We'll never share your email"
inputProps={{
"aria-describedby": "email-description",
"aria-invalid": "true", // When field has errors
}}
/>Manual ARIA Configuration
For custom accessibility needs, you can override ARIA attributes:
<InputField
name="username"
label="Username"
inputProps={{
"aria-label": "Choose a unique username",
"aria-describedby": "username-help",
"aria-required": "true",
}}
/>Screen Reader Support
Error Messages
Error messages are automatically associated with fields for screen readers:
// Error message is automatically linked to field
<InputField
name="email"
label="Email"
rules={{ required: "Email is required" }}
/>
// Screen reader will announce: "Email, required, Email is required"Form Status
Use the FormStatus component for screen reader announcements:
<FormStatus
isSubmitting={isSubmitting}
isSuccess={isSuccess}
error={error}
successMessage="Form submitted successfully"
// Screen reader will announce status changes
/>Field Descriptions
Provide helpful descriptions for screen readers:
<InputField
name="password"
label="Password"
description="Must be at least 8 characters with uppercase and number"
inputProps={{
"aria-describedby": "password-requirements",
}}
/>Keyboard Navigation
Tab Order
Fields are automatically included in the tab order:
// Tab order: name → email → phone → submit
<ZodForm
config={{
fields: [
FormFieldHelpers.input("name", "Name"),
FormFieldHelpers.input("email", "Email"),
FormFieldHelpers.input("phone", "Phone"),
],
onSubmit: handleSubmit,
}}
/>Custom Tab Index
Control tab order with custom tab indices:
<InputField
name="priority"
label="Priority"
inputProps={{ tabIndex: 1 }}
/>
<InputField
name="description"
label="Description"
inputProps={{ tabIndex: 2 }}
/>Keyboard Shortcuts
HeroUI components support standard keyboard shortcuts:
- Tab: Move to next field
- Shift+Tab: Move to previous field
- Enter: Submit form (on submit button)
- Escape: Close dropdowns/selects
- Arrow keys: Navigate select options
Focus Management
Focus Indicators
All fields have visible focus indicators:
// Focus ring is automatically applied
<InputField name="email" label="Email" />Programmatic Focus
Control focus programmatically:
import { useForm } from "react-hook-form";
function MyForm() {
const { setFocus } = useForm();
const handleError = (fieldName: string) => {
// Focus on field with error
setFocus(fieldName);
};
return (
<ZodForm
config={{
fields: [/* ... */],
onError: handleError,
}}
/>
);
}Focus Trapping
For modal forms, implement focus trapping:
import { useEffect, useRef } from "react";
function ModalForm() {
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
const form = formRef.current;
if (!form) return;
const focusableElements = form.querySelectorAll(
'input, button, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
form.addEventListener('keydown', handleKeyDown);
return () => form.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<form ref={formRef}>
<ZodForm config={{ /* ... */ }} />
</form>
);
}Color and Contrast
High Contrast Support
HeroUI components support high contrast mode:
// Components automatically adapt to high contrast
<InputField
name="email"
label="Email"
inputProps={{
// High contrast colors are applied automatically
}}
/>Color Blind Support
Use semantic colors and additional indicators:
// Use semantic colors for status
<FormStatus
isSuccess={isSuccess}
error={error}
// Green for success, red for error (not just colors)
successMessage="✓ Form submitted successfully"
errorMessage="✗ Please check your input"
/>Custom Color Schemes
Override colors for accessibility:
<ConfigProvider
defaults={{
input: {
color: "primary", // Use semantic colors
},
submitButton: {
color: "primary",
},
}}
>
<MyForm />
</ConfigProvider>Form Labels and Descriptions
Required Field Indicators
Clearly indicate required fields:
<InputField
name="email"
label="Email Address *"
rules={{ required: "Email is required" }}
inputProps={{
"aria-required": "true",
}}
/>Field Groups
Group related fields with proper labels:
<fieldset>
<legend>Contact Information</legend>
<InputField name="firstName" label="First Name" />
<InputField name="lastName" label="Last Name" />
<InputField name="email" label="Email" />
</fieldset>Help Text
Provide helpful descriptions:
<InputField
name="password"
label="Password"
description="Must be at least 8 characters"
inputProps={{
"aria-describedby": "password-help",
}}
/>
<div id="password-help" className="sr-only">
Password requirements: at least 8 characters, one uppercase letter, one number
</div>Error Handling
Error Announcements
Errors are automatically announced to screen readers:
// Screen reader will announce: "Email, invalid, Please enter a valid email"
<InputField
name="email"
label="Email"
rules={{
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Please enter a valid email"
}
}}
/>Error Summary
Provide an error summary for complex forms:
function ErrorSummary({ errors }: { errors: FieldErrors }) {
const errorList = Object.entries(errors).map(([field, error]) => ({
field,
message: error?.message,
}));
if (errorList.length === 0) return null;
return (
<div role="alert" aria-live="polite" className="error-summary">
<h3>Please correct the following errors:</h3>
<ul>
{errorList.map(({ field, message }) => (
<li key={field}>
<a href={`#${field}`} onClick={() => setFocus(field)}>
{message}
</a>
</li>
))}
</ul>
</div>
);
}Testing Accessibility
Screen Reader Testing
Test with screen readers:
# Install screen reader testing tools
npm install --save-dev @testing-library/jest-axe
# Add to test setup
import { toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);// Test for accessibility violations
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
test('form should not have accessibility violations', async () => {
const { container } = render(<MyForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});Keyboard Testing
Test keyboard navigation:
// Cypress keyboard testing
describe('Keyboard Navigation', () => {
it('should navigate fields with Tab key', () => {
cy.get('[name="name"]').focus();
cy.get('[name="name"]').tab();
cy.get('[name="email"]').should('be.focused');
});
it('should submit form with Enter key', () => {
cy.get('[data-testid="submit-button"]').focus();
cy.get('[data-testid="submit-button"]').type('{enter}');
cy.get('[data-testid="success-message"]').should('be.visible');
});
});Color Contrast Testing
Test color contrast:
// Test color contrast ratios
import { getContrast } from 'color2k';
test('error text has sufficient contrast', () => {
const errorColor = '#dc2626'; // red-600
const backgroundColor = '#ffffff'; // white
const contrast = getContrast(errorColor, backgroundColor);
// WCAG AA requires 4.5:1 for normal text
expect(contrast).toBeGreaterThanOrEqual(4.5);
});Best Practices
1. Semantic HTML
Use proper semantic elements:
// ✅ Good: Semantic form structure
<form role="form">
<fieldset>
<legend>User Information</legend>
<InputField name="name" label="Full Name" />
<InputField name="email" label="Email" />
</fieldset>
</form>2. Clear Labels
Provide clear, descriptive labels:
// ✅ Good: Clear labels
<InputField name="phone" label="Phone Number" />
<InputField name="email" label="Email Address" />
// ❌ Bad: Unclear labels
<InputField name="field1" label="Field" />
<InputField name="data" label="Info" />3. Error Messages
Provide helpful error messages:
// ✅ Good: Specific error messages
rules={{
required: "Email address is required",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Please enter a valid email address"
}
}}
// ❌ Bad: Generic error messages
rules={{
required: "Required",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Invalid"
}
}}4. Loading States
Indicate loading states clearly:
<FormStatus
isSubmitting={isSubmitting}
isSuccess={isSuccess}
error={error}
// Screen reader announces: "Form is submitting" or "Form submitted successfully"
/>5. Progress Indicators
Show progress for multi-step forms:
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 3;
return (
<div>
<div role="progressbar" aria-valuenow={currentStep} aria-valuemax={totalSteps}>
Step {currentStep} of {totalSteps}
</div>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${(currentStep / totalSteps) * 100}%` }}
/>
</div>
{/* Form content */}
</div>
);
}Accessibility Checklist
✅ Form Structure
- All fields have labels
- Required fields are marked
- Error messages are associated with fields
- Form has proper semantic structure
✅ Keyboard Navigation
- All fields are keyboard accessible
- Tab order is logical
- Focus indicators are visible
- Keyboard shortcuts work
✅ Screen Reader Support
- All content is announced
- Error messages are announced
- Status changes are announced
- Form structure is clear
✅ Visual Design
- Sufficient color contrast
- High contrast mode support
- Color is not the only indicator
- Text is readable
✅ Testing
- Tested with screen reader
- Tested with keyboard only
- Tested with high contrast
- Automated accessibility tests pass