Files
9c92be9d-7ee2-4111-9f25-4a3…/docs/ACCESSIBILITY.md
vitalijmulika ddd228ed76 Initial commit
2025-12-12 17:10:17 +02:00

501 lines
10 KiB
Markdown

# 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
```tsx
interface ButtonProps {
text: string;
onClick?: () => void;
className?: string;
// Accessibility props
disabled?: boolean;
ariaLabel?: string;
type?: "button" | "submit" | "reset";
}
```
### Implementation Pattern
```tsx
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 `text` content 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 cursor
- `disabled:opacity-50` - Reduces opacity for visual feedback
## Media Components
### Images
**Required Props:**
```tsx
interface ImageProps {
imageSrc: string;
imageAlt?: string; // Empty string for decorative images
className?: string;
}
```
**Implementation:**
```tsx
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:**
```tsx
interface VideoProps {
videoSrc: string;
videoAriaLabel?: string;
className?: string;
}
```
**Implementation:**
```tsx
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:
```tsx
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:
```tsx
<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
```tsx
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:
```tsx
// 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:
```tsx
<h1> // Page title (once per page)
<h2> // Section titles
<h3> // Subsection titles
<h4> // Card/component titles
```
**Example:**
```tsx
<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
```tsx
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 using `htmlFor` and `id`
- Mark required fields visually and semantically
- Use `aria-describedby` for error messages or hints
### Form Validation
```tsx
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:
```tsx
// ❌ 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:
```tsx
// 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:
```tsx
<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:
```tsx
<span className="sr-only">Additional context for screen readers</span>
```
Tailwind's `sr-only` 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;
}
```
## ARIA Roles
Use ARIA roles when semantic HTML isn't sufficient:
```tsx
// 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:
```tsx
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 `ariaLabel` prop (optional with sensible fallback)
- [ ] Add `type` prop for buttons (default: `"button"`)
- [ ] Add `disabled` prop 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 `imageAlt` prop
- [ ] Images: Use `aria-hidden={true}` when alt is empty (decorative)
- [ ] Videos: Add `videoAriaLabel` prop with sensible default
- [ ] Provide meaningful default labels
### Section Components
- [ ] Use semantic HTML (`<section>`, `<header>`, `<nav>`, `<footer>`)
- [ ] Add `ariaLabel` prop with sensible default
- [ ] Follow proper heading hierarchy (h1 → h2 → h3)
- [ ] Use `w-full py-20` for section spacing (except hero/footer)
- [ ] Use `w-content-width mx-auto` for content wrapper
### Form Components
- [ ] Associate labels with inputs using `htmlFor` and `id`
- [ ] Mark required fields semantically
- [ ] Use `aria-invalid` for validation errors
- [ ] Use `aria-describedby` for 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`)