| name | accessibility-patterns |
| description | Build accessible React components with WCAG 2.1 Level AA compliance for Hara Vital. Use when creating interactive elements, forms, modals, bottom sheets, or any user-facing components. Triggers on "accessibility", "a11y", "ARIA", "keyboard", "focus", "screen reader", "WCAG". |
Accessibility Patterns for Hara Vital
Build WCAG 2.1 Level AA compliant React components with Tailwind CSS.
Existing Accessibility in Hara
The codebase already has:
role="dialog" + aria-modal="true" + aria-labelledby on BottomSheet
role="region" + aria-label on card deck
aria-label on navigation buttons
- Semantic HTML throughout
Quick Reference: Interactive Elements
Buttons
{}
<button className="btn-press-glow bg-brand text-white rounded-full px-6 py-4">
Contactar por WhatsApp
</button>
{}
<button aria-label="Anterior profesional" className="w-12 h-12 rounded-full">
<svg>...</svg>
</button>
{}
<div onClick={handleClick} className="cursor-pointer">Click me</div>
{}
<button onClick={handleClose}><svg>...</svg></button>
Links
{}
<a href={`/p/${slug}`}>Ver perfil completo</a>
{}
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer"
aria-label="Contactar a María por WhatsApp (abre nueva pestaña)">
Contactar
</a>
{}
<a href="/p/maria">Click aquí</a>
Quick Reference: Forms
{}
<label htmlFor="email" className="text-sm font-medium text-foreground">
Email
</label>
<input id="email" type="email" value={email} onChange={...}
aria-required="true"
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && (
<p id="email-error" role="alert" className="text-sm text-danger">
{error}
</p>
)}
{}
<input placeholder="Email" />
Quick Reference: Modals & Bottom Sheets
Hara's BottomSheet already implements most of this. Follow the same pattern for new modals:
{}
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onKeyDown={(e) => e.key === 'Escape' && handleClose()}
>
<h2 id="modal-title">{title}</h2>
{}
</div>
Focus Trap (pending implementation)
import { useRef, useEffect } from 'react';
function useFocusTrap(ref: React.RefObject<HTMLElement>, isActive: boolean) {
useEffect(() => {
if (!isActive || !ref.current) return;
const element = ref.current;
const focusable = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
first?.focus();
const handleTab = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last?.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first?.focus();
}
};
element.addEventListener('keydown', handleTab);
return () => element.removeEventListener('keydown', handleTab);
}, [ref, isActive]);
}
Quick Reference: Keyboard Navigation
Tabindex Rules
| Value | Use Case |
|---|
0 | Element should be in tab order |
-1 | Focusable via JS only, not tab |
>0 | NEVER USE — breaks natural flow |
Swipe Alternatives for Keyboard
The card deck uses swipe gestures. For keyboard users:
{}
<button
onClick={() => setCurrentIndex(i => i - 1)}
aria-label="Anterior profesional"
tabIndex={0}
>
{}
</button>
Quick Reference: Live Regions
{}
<div aria-live="polite" aria-atomic="true" className="sr-only">
Mostrando profesional {currentIndex + 1} de {total}
</div>
{}
<div role="alert" className="text-danger">
{errorMessage}
</div>
{}
<div aria-busy={loading} aria-live="polite">
{loading ? <LoadingSkeleton /> : content}
</div>
Quick Reference: Images
{}
<img src={photoUrl} alt={`Foto de ${professionalName}`} />
{}
<img src={illustration} alt="" aria-hidden="true" />
{}
<div className="w-16 h-16 bg-gradient-to-br from-brand-weak to-info-weak rounded-3xl"
role="img" aria-label={`Avatar de ${name}`} />
Color Contrast
Hara's palette is designed for AA compliance:
text-foreground (#1F1A24) on bg-background (#FBF7F2) = 12.3:1 ratio
text-muted (#6B6374) on bg-background (#FBF7F2) = 4.8:1 ratio (passes AA)
text-brand (#4B2BBF) on bg-background (#FBF7F2) = 7.2:1 ratio
If adding new colors, verify contrast at WebAIM Contrast Checker.
Testing Checklist
Before submitting accessible components: