Accessibility Patterns
Skill Purpose: WCAG compliance implementation, screen reader support, and inclusive design patterns
Core Skill Pattern
Objective: Establish comprehensive accessibility strategy with WCAG compliance, screen reader support, and inclusive design practices.
Universal Pattern:
- •Define accessibility standards and compliance requirements
- •Create semantic HTML and ARIA implementation patterns
- •Establish screen reader and keyboard navigation procedures
- •Set up color contrast and visual accessibility validation
- •Create accessibility testing and documentation procedures
Key Decisions (Project-Specific):
- •WCAG compliance level and target standards
- •Screen reader support strategy and testing approach
- •Keyboard navigation patterns and shortcuts
- •Color contrast requirements and validation tools
- •Accessibility testing framework and automation
Project-Specific Implementation Notes
Customize per project:
- •WCAG level based on compliance requirements (AA, AAA)
- •Screen reader support based on target user demographics
- •Keyboard navigation based on application complexity
- •Color contrast based on brand guidelines and accessibility
- •Testing depth based on criticality and resources
Example Implementation (Next.js Accessibility Pattern)
Note: This is an example pattern using WCAG 2.1 AA compliance with React/Next.js. Adapt accessibility standards and patterns based on your specific project requirements and compliance needs.
Prerequisites (Example)
- •Next.js project structure established
- •Accessibility requirements and compliance level defined
- •Screen reader testing tools configured
- •Color contrast validation tools selected
Example: Accessibility Implementation
Framework-Specific Example: This demonstrates accessibility patterns with Next.js and shadcn/ui. Adapt for your accessibility framework and component library requirements.
Accessibility Configuration
typescript
// lib/accessibility-config.ts
export interface AccessibilityConfig {
wcagLevel: 'A' | 'AA' | 'AAA';
language: string;
skipNavigation: boolean;
focusManagement: boolean;
colorContrast: {
normalText: number;
largeText: number;
interactive: number;
};
screenReader: {
announcements: boolean;
landmarks: boolean;
descriptions: boolean;
};
}
export const accessibilityConfig: AccessibilityConfig = {
wcagLevel: 'AA',
language: 'en',
skipNavigation: true,
focusManagement: true,
colorContrast: {
normalText: 4.5,
largeText: 3.0,
interactive: 3.0,
},
screenReader: {
announcements: true,
landmarks: true,
descriptions: true,
},
};
Semantic HTML Components
typescript
// components/ui/accessible-button.tsx
import React, { forwardRef } from 'react';
import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
interface AccessibleButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
'aria-label'?: string;
'aria-describedby'?: string;
'aria-expanded'?: boolean;
'aria-pressed'?: boolean;
}
const AccessibleButton = forwardRef<HTMLButtonElement, AccessibleButtonProps>(
({ className, variant = 'default', size = 'default', asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(
// Base styles with accessibility considerations
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium',
'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
// High contrast focus indicators
'focus:outline-none focus:ring-2 focus:ring-offset-2',
{
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
'border border-input bg-background hover:bg-accent hover:text-accent-foreground': variant === 'outline',
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
'text-primary underline-offset-4 hover:underline': variant === 'link',
},
{
'h-10 px-4 py-2': size === 'default',
'h-9 rounded-md px-3': size === 'sm',
'h-11 rounded-md px-8': size === 'lg',
'h-10 w-10': size === 'icon',
},
className
)}
ref={ref}
{...props}
/>
);
}
);
AccessibleButton.displayName = 'AccessibleButton';
export { AccessibleButton };
Skip Navigation Component
typescript
// components/ui/skip-navigation.tsx
import React from 'react';
import { AccessibleButton } from './accessible-button';
export function SkipNavigation() {
return (
<AccessibleButton
asChild
className="absolute left-0 top-0 -translate-x-full -translate-y-full bg-primary text-primary-foreground p-2 focus:translate-x-0 focus:translate-y-0 z-50"
onClick={() => {
const mainContent = document.getElementById('main-content');
mainContent?.focus();
}}
>
<a href="#main-content" className="no-underline">
Skip to main content
</a>
</AccessibleButton>
);
}
Accessible Form Components
typescript
// components/ui/accessible-input.tsx
import React, { forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface AccessibleInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
helperText?: string;
required?: boolean;
}
const AccessibleInput = forwardRef<HTMLInputElement, AccessibleInputProps>(
({ label, error, helperText, required, id, className, ...props }, ref) => {
const inputId = id || `input-${React.useId()}`;
const errorId = error ? `${inputId}-error` : undefined;
const helperId = helperText ? `${inputId}-helper` : undefined;
return (
<div className="space-y-2">
<label
htmlFor={inputId}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{label}
{required && <span className="text-destructive ml-1" aria-label="required">*</span>}
</label>
<input
id={inputId}
ref={ref}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
'ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium',
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-destructive focus-visible:ring-destructive',
className
)}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={cn(errorId, helperId)}
aria-required={required}
{...props}
/>
{error && (
<p id={errorId} className="text-sm text-destructive" role="alert">
{error}
</p>
)}
{helperText && !error && (
<p id={helperId} className="text-sm text-muted-foreground">
{helperText}
</p>
)}
</div>
);
}
);
AccessibleInput.displayName = 'AccessibleInput';
export { AccessibleInput };
Screen Reader Announcements
typescript
// lib/accessibility-announcer.ts
export class AccessibilityAnnouncer {
private static announcementRegion: HTMLElement | null = null;
static init() {
// Create live region for screen reader announcements
if (typeof window !== 'undefined' && !this.announcementRegion) {
this.announcementRegion = document.createElement('div');
this.announcementRegion.setAttribute('aria-live', 'polite');
this.announcementRegion.setAttribute('aria-atomic', 'true');
this.announcementRegion.className = 'sr-only';
document.body.appendChild(this.announcementRegion);
}
}
static announce(message: string, priority: 'polite' | 'assertive' = 'polite') {
if (!this.announcementRegion) {
this.init();
}
if (this.announcementRegion) {
this.announcementRegion.setAttribute('aria-live', priority);
this.announcementRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
if (this.announcementRegion) {
this.announcementRegion.textContent = '';
}
}, 1000);
}
}
static announcePageChange(pageTitle: string) {
this.announce(`Navigated to ${pageTitle}`, 'assertive');
}
static announceError(error: string) {
this.announce(`Error: ${error}`, 'assertive');
}
static announceSuccess(message: string) {
this.announce(`Success: ${message}`, 'polite');
}
}
Focus Management
typescript
// lib/focus-management.ts
export class FocusManager {
static trapFocus(container: HTMLElement) {
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as NodeListOf<HTMLElement>;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleTabKey = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
};
container.addEventListener('keydown', handleTabKey);
// Return cleanup function
return () => {
container.removeEventListener('keydown', handleTabKey);
};
}
static setInitialFocus(container: HTMLElement) {
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as NodeListOf<HTMLElement>;
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
}
static restoreFocus(previousElement: HTMLElement | null) {
if (previousElement) {
previousElement.focus();
}
}
}
Color Contrast Validation
typescript
// lib/color-contrast.ts
export class ColorContrastValidator {
static hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
static getLuminance(r: number, g: number, b: number): number {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
static getContrastRatio(color1: string, color2: string): number {
const rgb1 = this.hexToRgb(color1);
const rgb2 = this.hexToRgb(color2);
if (!rgb1 || !rgb2) return 0;
const lum1 = this.getLuminance(rgb1.r, rgb1.g, rgb1.b);
const lum2 = this.getLuminance(rgb2.r, rgb2.g, rgb2.b);
const brightest = Math.max(lum1, lum2);
const darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
static validateContrast(
foreground: string,
background: string,
isLargeText: boolean = false
): { valid: boolean; ratio: number; required: number } {
const ratio = this.getContrastRatio(foreground, background);
const required = isLargeText ? 3.0 : 4.5;
return {
valid: ratio >= required,
ratio: Math.round(ratio * 100) / 100,
required
};
}
}
Accessibility Testing Patterns
typescript
// __tests__/accessibility/accessibility.test.tsx
import { render, screen } from '@testing-library/react';
import { AccessibleButton } from '@/components/ui/accessible-button';
import userEvent from '@testing-library/user-event';
describe('Accessibility Components', () => {
describe('AccessibleButton', () => {
it('should have proper ARIA attributes', () => {
render(
<AccessibleButton
aria-label="Close dialog"
aria-expanded={false}
>
×
</AccessibleButton>
);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-label', 'Close dialog');
expect(button).toHaveAttribute('aria-expanded', 'false');
});
it('should be keyboard accessible', async () => {
const handleClick = jest.fn();
render(<AccessibleButton onClick={handleClick}>Click me</AccessibleButton>);
const button = screen.getByRole('button');
button.focus();
expect(button).toHaveFocus();
await userEvent.keyboard('{Enter}');
expect(handleClick).toHaveBeenCalled();
handleClick.mockClear();
await userEvent.keyboard(' ');
expect(handleClick).toHaveBeenCalled();
});
it('should have visible focus indicators', () => {
render(<AccessibleButton>Test</AccessibleButton>);
const button = screen.getByRole('button');
expect(button).toHaveClass('focus-visible:outline-none');
expect(button).toHaveClass('focus-visible:ring-2');
});
});
});
Best Practices
- •Semantic HTML - Use proper HTML elements for their intended purpose
- •ARIA attributes - Use ARIA to enhance, not replace, semantic HTML
- •Keyboard navigation - Ensure all interactive elements are keyboard accessible
- •Focus management - Provide clear focus indicators and logical tab order
- •Color contrast - Meet WCAG AA contrast ratios for text and interactive elements
Stop Conditions
STOP and escalate if:
- •Accessibility requirements unclear or incomplete
- •WCAG compliance level not defined
- •Screen reader testing strategy missing
- •Color contrast validation not implemented
Skill Version: 1.0.0