Accessibility Guidelines
WCAG 2.1 AA Compliance
All components and features must meet WCAG 2.1 Level AA standards.
Contrast Requirements
- •Normal text: Minimum 4.5:1 contrast ratio against background
- •Large text (18px+ or 14px+ bold): Minimum 3:1 contrast ratio
- •UI components: Minimum 3:1 contrast ratio for interactive elements and their states
- •Focus indicators: Minimum 3:1 contrast ratio, must be visible on all interactive elements
Keyboard Navigation
- •All interactive elements must be keyboard accessible
- •Tab order must follow logical reading order
- •Focus indicators must be visible and clear
- •No keyboard traps (users can navigate away from all elements)
- •Support standard keyboard patterns (Enter, Space, Escape, Arrow keys where appropriate)
Screen Reader Support
- •Use semantic HTML elements (button, nav, main, aside, header, footer)
- •Maintain proper heading hierarchy (h1 -> h2 -> h3, no skipping levels)
- •Associate labels with form inputs using
htmlFor/idoraria-label - •Provide descriptive alt text for meaningful images
- •Use empty alt (
alt="") for decorative images - •Announce dynamic content changes with ARIA live regions
- •Include
role="alert"for error messages
ARIA Attributes
When building custom components that don't use native HTML elements:
- •Add appropriate
roleattributes (e.g.,role="button",role="dialog") - •Use
aria-labeloraria-labelledbyfor accessible names - •Use
aria-describedbyfor additional descriptions - •Use
aria-expandedfor expandable elements - •Use
aria-hidden="true"for decorative elements - •Use
aria-livefor dynamic content announcements - •Use
aria-invalidandaria-describedbyfor form validation
Mobile Accessibility
Touch Targets
- •Minimum size: 44x44 pixels for all interactive elements
- •Adequate spacing between touch targets (at least 8px)
- •Applies to buttons, links, form inputs, and custom controls
Text Scaling
- •Use relative units (rem, em) for font sizes, not fixed pixels
- •Support browser zoom up to 200% without loss of functionality
- •Avoid horizontal scrolling at 200% zoom
Component-Specific Guidelines
Buttons (AppButton)
- •Use semantic
<button>elements, not divs with click handlers - •Include
type="button"to prevent form submission - •Provide
aria-labelfor icon-only buttons - •Show loading state with
aria-busy="true" - •Disable with
disabledattribute, not just visual styling - •Ensure 44x44px minimum size on mobile
Forms (AppInput)
- •Always associate labels with inputs
- •Use
aria-invalid="true"for validation errors - •Use
aria-describedbyto link error messages - •Mark required fields with
aria-required="true"orrequiredattribute - •Announce errors with
role="alert"oraria-live="assertive" - •Group related inputs with
<fieldset>and<legend>
Modals and Dialogs
- •Use
role="dialog"orrole="alertdialog" - •Trap focus within the modal when open
- •Return focus to trigger element when closed
- •Support Escape key to close
- •Use
aria-modal="true"to indicate modal behavior - •Provide accessible close button with clear label
- •Modal overlays must NOT be keyboard-interactive: Use
role="presentation"andaria-hidden="true"on backdrop overlays
Common Patterns
Focus Management
typescript
// Complete modal/sidebar focus management pattern
const MyModal = ({ isOpen, onClose }) => {
const modalRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
// Focus trap
useEffect(() => {
if (!isOpen) return;
const modal = modalRef.current;
if (!modal) return;
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleTab = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
};
modal.addEventListener('keydown', handleTab);
firstElement?.focus();
return () => modal.removeEventListener('keydown', handleTab);
}, [isOpen]);
// Return focus to trigger element when closed
const handleClose = useCallback(() => {
onClose();
setTimeout(() => triggerRef.current?.focus(), 100);
}, [onClose]);
return (
<>
{/* Overlay - NOT keyboard interactive */}
<div
className="fixed inset-0 bg-black/50"
onClick={handleClose}
role="presentation"
aria-hidden="true"
/>
{/* Modal content */}
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<h2 id="modal-title">Modal Title</h2>
<button onClick={handleClose}>Close</button>
</div>
</>
);
};
Screen Reader Announcements
typescript
// Announce to screen reader
const announce = (message: string, priority: 'polite' | 'assertive' = 'polite') => {
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', priority);
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => announcement.remove(), 1000);
};
Visually Hidden Class
css
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
Testing Checklist
Before marking a component complete:
- • Test with keyboard only (no mouse)
- • Test with screen reader (NVDA, JAWS, or VoiceOver)
- • Verify color contrast with browser dev tools
- • Test at 200% browser zoom
- • Test on mobile device with touch
- • Validate HTML with W3C validator
- • Run automated accessibility audit (Lighthouse, axe)