10 KiB
Accessibility Standards
This document outlines accessibility (a11y) requirements for all components in the library, ensuring compatibility with screen readers and assistive technologies.
Interactive Components
For buttons, links, and other interactive elements.
Required Props
interface ButtonProps {
text: string;
onClick?: () => void;
className?: string;
// Accessibility props
disabled?: boolean;
ariaLabel?: string;
type?: "button" | "submit" | "reset";
}
Implementation Pattern
const Button = ({
text,
onClick,
className = "",
disabled = false,
ariaLabel,
type = "button",
}: ButtonProps) => {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel || text}
className={cls(
"base-button-styles",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
>
{text}
</button>
);
};
Key Points
ariaLabel:
- Optional prop with sensible fallback
- Falls back to
textcontent for buttons - Provides context for screen readers
type:
- Default:
"button" - Options:
"button" | "submit" | "reset" - Prevents accidental form submission
disabled:
- Default:
false - Includes visual disabled states:
disabled:cursor-not-allowed- Shows not-allowed cursordisabled:opacity-50- Reduces opacity for visual feedback
Media Components
Images
Required Props:
interface ImageProps {
imageSrc: string;
imageAlt?: string; // Empty string for decorative images
className?: string;
}
Implementation:
const ImageComponent = ({
imageSrc,
imageAlt = "",
className = "",
}: ImageProps) => {
return (
<Image
src={imageSrc}
alt={imageAlt}
aria-hidden={imageAlt === ""}
className={className}
/>
);
};
Key Points:
imageAlt- Alt text for images- Provide descriptive alt text for meaningful images
- Use empty string (
"") for decorative images
aria-hidden={true}- When alt is empty, mark as decorative- Screen readers will skip decorative images
Videos
Required Props:
interface VideoProps {
videoSrc: string;
videoAriaLabel?: string;
className?: string;
}
Implementation:
const VideoComponent = ({
videoSrc,
videoAriaLabel = "Video content",
className = "",
}: VideoProps) => {
return (
<video
src={videoSrc}
aria-label={videoAriaLabel}
autoPlay
loop
muted
playsInline
className={className}
/>
);
};
Key Points:
videoAriaLabel- Descriptive label for video element- Default: Sensible fallback like "Video content" or "Hero video"
- Always include for screen reader context
Media Content Pattern
For components supporting both images and videos:
interface HeroProps {
imageSrc?: string;
imageAlt?: string;
videoSrc?: string;
videoAriaLabel?: string;
}
const Hero = ({
imageSrc,
imageAlt = "",
videoSrc,
videoAriaLabel = "Hero video",
}: HeroProps) => {
return (
<>
{videoSrc ? (
<video
src={videoSrc}
aria-label={videoAriaLabel}
autoPlay
loop
muted
playsInline
/>
) : (
imageSrc && (
<Image
src={imageSrc}
alt={imageAlt}
aria-hidden={imageAlt === ""}
/>
)
)}
</>
);
};
Section Components
Semantic HTML
Use semantic HTML elements for proper document structure:
<section> // For sections of content
<header> // For page/section headers
<nav> // For navigation
<footer> // For page/section footers
<article> // For self-contained content
<aside> // For tangentially related content
<main> // For main content area
Section Pattern
interface SectionProps {
title: string;
description: string;
ariaLabel?: string;
className?: string;
}
const Section = ({
title,
description,
ariaLabel = "Section name",
className = "",
}: SectionProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("w-full py-20", className)}
>
<div className="w-content-width mx-auto">
<h2>{title}</h2>
<p>{description}</p>
</div>
</section>
);
};
Sensible Default aria-labels
Each section type should have a descriptive default:
// Hero section
ariaLabel = "Hero section"
// About section
ariaLabel = "About section"
// Feature section
ariaLabel = "Features section"
// Testimonial section
ariaLabel = "Testimonials section"
// Footer
ariaLabel = "Footer"
// Navigation
ariaLabel = "Navigation"
Heading Hierarchy
Maintain proper heading levels:
<h1> // Page title (once per page)
<h2> // Section titles
<h3> // Subsection titles
<h4> // Card/component titles
Example:
<section aria-label="Features section">
<h2>Our Features</h2> {/* Section title */}
<div>
<h3>Feature One</h3> {/* Feature title */}
<p>Description...</p>
</div>
</section>
Form Components
Input Fields
interface InputProps {
label: string;
id: string;
type?: string;
required?: boolean;
ariaLabel?: string;
ariaDescribedBy?: string;
}
const Input = ({
label,
id,
type = "text",
required = false,
ariaLabel,
ariaDescribedBy,
}: InputProps) => {
return (
<div>
<label htmlFor={id}>
{label}
{required && <span aria-label="required">*</span>}
</label>
<input
id={id}
type={type}
required={required}
aria-label={ariaLabel || label}
aria-describedby={ariaDescribedBy}
/>
</div>
);
};
Key Points:
- Always associate
<label>with input usinghtmlForandid - Mark required fields visually and semantically
- Use
aria-describedbyfor error messages or hints
Form Validation
const [error, setError] = useState("");
<input
aria-invalid={!!error}
aria-describedby={error ? "error-message" : undefined}
/>
{error && (
<p id="error-message" role="alert">
{error}
</p>
)}
Focus Management
Focus Indicators
Never remove focus outlines without providing alternatives:
// ❌ WRONG
className="outline-none"
// ✅ CORRECT - Custom focus indicator
className="focus:outline-none focus:ring-2 focus:ring-foreground/50"
Focus Trap (for modals/dialogs)
When implementing modals or dialogs, ensure:
- Focus moves to modal when opened
- Tab/Shift+Tab cycles through modal elements only
- Focus returns to trigger element when closed
- Escape key closes modal
Keyboard Navigation
Interactive Elements
All interactive elements must be keyboard accessible:
// Buttons and links work by default
<button onClick={handleClick}>Click me</button>
<a href="/page">Link</a>
// Custom interactive elements need tabIndex and keyboard handlers
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
Custom button
</div>
Skip Links
For navigation-heavy pages, provide skip links:
<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>
Screen Reader Only Content
Use the sr-only class for content that should only be read by screen readers:
<span className="sr-only">Additional context for screen readers</span>
Tailwind's sr-only class:
.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;
}
ARIA Roles
Use ARIA roles when semantic HTML isn't sufficient:
// Navigation
<nav role="navigation" aria-label="Main navigation">
// Button (for non-button elements)
<div role="button" tabIndex={0}>
// Alert/Status messages
<div role="alert">Error message</div>
<div role="status">Loading...</div>
// Presentation (decorative)
<div role="presentation">
// Dialog/Modal
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
Loading States
Provide feedback for loading states:
const [isLoading, setIsLoading] = useState(false);
<button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? (
<>
<span className="sr-only">Loading...</span>
<Spinner aria-hidden="true" />
</>
) : (
"Submit"
)}
</button>
Accessibility Checklist
Interactive Components
- Add
ariaLabelprop (optional with sensible fallback) - Add
typeprop for buttons (default:"button") - Add
disabledprop with visual states - Include disabled state styling (
disabled:cursor-not-allowed disabled:opacity-50) - Ensure keyboard accessibility (Enter/Space for custom elements)
- Provide custom focus indicators if removing default outline
Media Components
- Images: Add
imageAltprop - Images: Use
aria-hidden={true}when alt is empty (decorative) - Videos: Add
videoAriaLabelprop with sensible default - Provide meaningful default labels
Section Components
- Use semantic HTML (
<section>,<header>,<nav>,<footer>) - Add
ariaLabelprop with sensible default - Follow proper heading hierarchy (h1 → h2 → h3)
- Use
w-full py-20for section spacing (except hero/footer) - Use
w-content-width mx-autofor content wrapper
Form Components
- Associate labels with inputs using
htmlForandid - Mark required fields semantically
- Use
aria-invalidfor validation errors - Use
aria-describedbyfor error messages/hints - Provide
role="alert"for error messages
General
- Test with keyboard navigation (Tab, Shift+Tab, Enter, Space, Escape)
- Test with screen reader (VoiceOver, NVDA, JAWS)
- Ensure sufficient color contrast (WCAG AA minimum)
- Provide focus indicators for all interactive elements
- Use semantic HTML before ARIA roles
- Include screen reader only text when needed (
sr-only)