setIsMenuOpen(false)}
+ aria-hidden="true"
+ />
+ )}
+ >
+ );
+}
diff --git a/src/app/components/accessibility/AccessibilityProvider.tsx b/src/app/components/accessibility/AccessibilityProvider.tsx
new file mode 100644
index 0000000..e73d5d7
--- /dev/null
+++ b/src/app/components/accessibility/AccessibilityProvider.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import React from 'react';
+import { AccessibilityNavigator } from './AccessibilityNavigator';
+import { ScreenReaderOptimizer } from './ScreenReaderOptimizer';
+import { ColorContrastChecker } from './ColorContrastChecker';
+import { AccessibilityTester } from './AccessibilityTester';
+
+interface AccessibilityProviderProps {
+ children: React.ReactNode;
+ enableNavigator?: boolean;
+ enableScreenReader?: boolean;
+ enableContrastChecker?: boolean;
+ enableTester?: boolean;
+ autoCheckContrast?: boolean;
+ autoCheckAccessibility?: boolean;
+}
+
+/**
+ * Comprehensive accessibility provider that wraps the application
+ * with all accessibility features enabled
+ */
+export function AccessibilityProvider({
+ children,
+ enableNavigator = true,
+ enableScreenReader = true,
+ enableContrastChecker = true,
+ enableTester = true,
+ autoCheckContrast = false,
+ autoCheckAccessibility = false,
+}: AccessibilityProviderProps) {
+ return (
+ <>
+ {/* Screen Reader Optimization */}
+ {enableScreenReader ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+ {/* Keyboard Navigation */}
+ {enableNavigator &&
}
+
+ {/* Color Contrast Checker */}
+ {enableContrastChecker && (
+
+ )}
+
+ {/* Accessibility Tester */}
+ {enableTester && (
+
+ )}
+ >
+ );
+}
+
+/**
+ * Hook to access accessibility features programmatically
+ */
+export { useAccessibilityCheck, useScreenReaderAnnouncement, useFocusTrap, useKeyboardNavigation } from '@/hooks/useAccessibility';
+
+/**
+ * Utility components for specific use cases
+ */
+export {
+ AccessibleDescription,
+ AccessibleLoading,
+ AccessibleError,
+ AccessibleSuccess,
+ AccessibleProgress,
+} from './ScreenReaderOptimizer';
diff --git a/src/app/components/accessibility/AccessibilityTester.tsx b/src/app/components/accessibility/AccessibilityTester.tsx
new file mode 100644
index 0000000..03c5bf2
--- /dev/null
+++ b/src/app/components/accessibility/AccessibilityTester.tsx
@@ -0,0 +1,305 @@
+'use client';
+
+import React, { useState } from 'react';
+import { useAccessibilityCheck } from '@/hooks/useAccessibility';
+import { AccessibilityIssue, getWCAGLevel } from '@/utils/accessibilityUtils';
+import { AlertCircle, CheckCircle, AlertTriangle, Info, Download } from 'lucide-react';
+
+interface AccessibilityTesterProps {
+ autoCheck?: boolean;
+ showWidget?: boolean;
+}
+
+export function AccessibilityTester({
+ autoCheck = false,
+ showWidget = true,
+}: AccessibilityTesterProps) {
+ const { containerRef, issues, isChecking, checkAccessibility } = useAccessibilityCheck(autoCheck);
+ const [isWidgetOpen, setIsWidgetOpen] = useState(false);
+ const [filterSeverity, setFilterSeverity] = useState
('all');
+
+ const filteredIssues =
+ filterSeverity === 'all'
+ ? issues
+ : issues.filter((issue) => issue.severity === filterSeverity);
+
+ const severityCounts = {
+ critical: issues.filter((i) => i.severity === 'critical').length,
+ serious: issues.filter((i) => i.severity === 'serious').length,
+ moderate: issues.filter((i) => i.severity === 'moderate').length,
+ minor: issues.filter((i) => i.severity === 'minor').length,
+ };
+
+ const wcagLevel = getWCAGLevel(issues);
+
+ const getSeverityIcon = (severity: string) => {
+ switch (severity) {
+ case 'critical':
+ return ;
+ case 'serious':
+ return ;
+ case 'moderate':
+ return ;
+ case 'minor':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getSeverityColor = (severity: string) => {
+ switch (severity) {
+ case 'critical':
+ return 'bg-red-50 border-red-200 text-red-800';
+ case 'serious':
+ return 'bg-orange-50 border-orange-200 text-orange-800';
+ case 'moderate':
+ return 'bg-yellow-50 border-yellow-200 text-yellow-800';
+ case 'minor':
+ return 'bg-blue-50 border-blue-200 text-blue-800';
+ default:
+ return 'bg-gray-50 border-gray-200 text-gray-800';
+ }
+ };
+
+ const getWCAGLevelColor = (level: string) => {
+ switch (level) {
+ case 'AAA':
+ return 'bg-green-100 text-green-800';
+ case 'AA':
+ return 'bg-blue-100 text-blue-800';
+ case 'A':
+ return 'bg-yellow-100 text-yellow-800';
+ case 'Fail':
+ return 'bg-red-100 text-red-800';
+ default:
+ return 'bg-gray-100 text-gray-800';
+ }
+ };
+
+ const exportReport = () => {
+ const report = {
+ timestamp: new Date().toISOString(),
+ wcagLevel,
+ totalIssues: issues.length,
+ severityCounts,
+ issues: issues.map((issue) => ({
+ severity: issue.severity,
+ type: issue.type,
+ element: issue.element,
+ message: issue.message,
+ wcagCriteria: issue.wcagCriteria,
+ suggestion: issue.suggestion,
+ })),
+ };
+
+ const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `accessibility-report-${Date.now()}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ if (!showWidget) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ {/* Accessibility Tester Widget Button */}
+
+
+ {/* Accessibility Tester Panel */}
+ {isWidgetOpen && (
+
+ {/* Header */}
+
+
+
Accessibility Tester
+
+
+
+ {/* WCAG Level Badge */}
+
+ WCAG Compliance:
+
+ {wcagLevel}
+
+
+
+
+
+ {issues.length > 0 && (
+
+ )}
+
+
+ {/* Summary */}
+ {issues.length > 0 && (
+
+
+
+
{severityCounts.critical}
+
Critical
+
+
+
+ {severityCounts.serious}
+
+
Serious
+
+
+
+ {severityCounts.moderate}
+
+
Moderate
+
+
+
{severityCounts.minor}
+
Minor
+
+
+
+ )}
+
+ {/* Filter */}
+ {issues.length > 0 && (
+
+
+
+
+ )}
+
+ {/* Issues List */}
+
+ {issues.length === 0 && !isChecking && (
+
+
+
No issues found!
+
Page passes basic accessibility checks.
+
+ )}
+
+ {isChecking && (
+
+
+
Checking accessibility...
+
+ )}
+
+ {filteredIssues.length > 0 && (
+
+ {filteredIssues.map((issue) => (
+
+
+ {getSeverityIcon(issue.severity)}
+
+
{issue.message}
+
+ Element: {issue.element}
+
+
+
+
+
+
+ WCAG:{' '}
+ {issue.wcagCriteria.join(', ')}
+
+
+ Suggestion: {issue.suggestion}
+
+
+
+ ))}
+
+ )}
+
+ {filteredIssues.length === 0 && issues.length > 0 && (
+
+
+
No {filterSeverity} issues found.
+
+ )}
+
+
+ {/* Footer */}
+
+
+ This tool performs automated checks for common accessibility issues. Manual testing
+ with assistive technologies is still recommended.
+
+
+
+ )}
+
+ {/* Overlay */}
+ {isWidgetOpen && (
+ setIsWidgetOpen(false)}
+ aria-hidden="true"
+ />
+ )}
+ >
+ );
+}
diff --git a/src/app/components/accessibility/ColorContrastChecker.tsx b/src/app/components/accessibility/ColorContrastChecker.tsx
new file mode 100644
index 0000000..006b040
--- /dev/null
+++ b/src/app/components/accessibility/ColorContrastChecker.tsx
@@ -0,0 +1,289 @@
+'use client';
+
+import React, { useState, useEffect, useCallback } from 'react';
+import { calculateContrastRatio, getComputedColor, ColorContrastResult } from '@/utils/accessibilityUtils';
+import { Eye, AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
+
+interface ColorPair {
+ foreground: string;
+ background: string;
+ element?: string;
+ location?: string;
+}
+
+interface ColorContrastCheckerProps {
+ autoCheck?: boolean;
+ showWidget?: boolean;
+}
+
+export function ColorContrastChecker({
+ autoCheck = false,
+ showWidget = true,
+}: ColorContrastCheckerProps) {
+ const [isChecking, setIsChecking] = useState(false);
+ const [colorPairs, setColorPairs] = useState<(ColorPair & { result: ColorContrastResult })[]>([]);
+ const [isWidgetOpen, setIsWidgetOpen] = useState(false);
+
+ const checkPageContrast = useCallback(() => {
+ setIsChecking(true);
+
+ // Find all text elements
+ const textElements = document.querySelectorAll(
+ 'p, h1, h2, h3, h4, h5, h6, span, a, button, label, li, td, th'
+ );
+
+ const pairs: (ColorPair & { result: ColorContrastResult })[] = [];
+ const checkedPairs = new Set
();
+
+ textElements.forEach((element) => {
+ const htmlElement = element as HTMLElement;
+
+ // Skip hidden elements
+ if (htmlElement.offsetParent === null) return;
+
+ const foreground = getComputedColor(htmlElement, 'color');
+ const background = getComputedColor(htmlElement, 'background-color');
+
+ // Create unique key for this color pair
+ const pairKey = `${foreground}-${background}`;
+
+ // Skip if already checked
+ if (checkedPairs.has(pairKey)) return;
+ checkedPairs.add(pairKey);
+
+ const result = calculateContrastRatio(foreground, background);
+
+ // Only include pairs that don't meet AA standards
+ if (!result.passes.aa) {
+ pairs.push({
+ foreground,
+ background,
+ element: htmlElement.tagName.toLowerCase(),
+ location: htmlElement.textContent?.substring(0, 50) || 'Unknown',
+ result,
+ });
+ }
+ });
+
+ setColorPairs(pairs);
+ setIsChecking(false);
+ }, []);
+
+ useEffect(() => {
+ if (autoCheck) {
+ const timer = setTimeout(checkPageContrast, 1000);
+ return () => clearTimeout(timer);
+ }
+ }, [autoCheck, checkPageContrast]);
+
+ const getStatusIcon = (result: ColorContrastResult) => {
+ if (result.passes.aa) {
+ return ;
+ } else if (result.passes.aaLarge) {
+ return ;
+ } else {
+ return ;
+ }
+ };
+
+ const getStatusText = (result: ColorContrastResult) => {
+ if (result.passes.aaa) return 'AAA';
+ if (result.passes.aa) return 'AA';
+ if (result.passes.aaLarge) return 'AA Large';
+ return 'Fail';
+ };
+
+ if (!showWidget) return null;
+
+ return (
+ <>
+ {/* Contrast Checker Widget Button */}
+
+
+ {/* Contrast Checker Panel */}
+ {isWidgetOpen && (
+
+ {/* Header */}
+
+
+
Color Contrast Checker
+
+
+
+
+
+ {/* Results */}
+
+ {colorPairs.length === 0 && !isChecking && (
+
+
+
No contrast issues found!
+
All text meets WCAG AA standards.
+
+ )}
+
+ {isChecking && (
+
+
+
Checking contrast...
+
+ )}
+
+ {colorPairs.length > 0 && (
+
+
+ Found {colorPairs.length} contrast issue{colorPairs.length !== 1 ? 's' : ''}
+
+
+ {colorPairs.map((pair, index) => (
+
+ {/* Color Swatches */}
+
+
+
+ Aa
+
+
+
+ {getStatusIcon(pair.result)}
+ {getStatusText(pair.result)}
+
+
+ Ratio: {pair.result.ratio}:1
+
+
+
+
+ {/* Element Info */}
+
+
+ Element:{' '}
+ {pair.element}
+
+
+ Text:{' '}
+
+ {pair.location}
+
+
+
+
Colors:
+
+
+ FG:
+
+ {pair.foreground}
+
+
+
+ BG:
+
+ {pair.background}
+
+
+
+
+
+
+ {/* WCAG Compliance */}
+
+
+
+ {pair.result.passes.aa ? (
+
+ ) : (
+
+ )}
+ AA (4.5:1)
+
+
+ {pair.result.passes.aaa ? (
+
+ ) : (
+
+ )}
+ AAA (7:1)
+
+
+ {pair.result.passes.aaLarge ? (
+
+ ) : (
+
+ )}
+ AA Large (3:1)
+
+
+ {pair.result.passes.aaaLarge ? (
+
+ ) : (
+
+ )}
+ AAA Large (4.5:1)
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ WCAG 2.1 requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for
+ large text (18pt+ or 14pt+ bold).
+
+
+
+ )}
+
+ {/* Overlay */}
+ {isWidgetOpen && (
+ setIsWidgetOpen(false)}
+ aria-hidden="true"
+ />
+ )}
+ >
+ );
+}
diff --git a/src/app/components/accessibility/README.md b/src/app/components/accessibility/README.md
new file mode 100644
index 0000000..a150b09
--- /dev/null
+++ b/src/app/components/accessibility/README.md
@@ -0,0 +1,285 @@
+# Accessibility Features - WCAG 2.1 AA Compliance
+
+This directory contains comprehensive accessibility features ensuring WCAG 2.1 AA compliance across the entire platform.
+
+## Components
+
+### 1. AccessibilityNavigator
+Provides keyboard navigation and skip links for efficient page navigation.
+
+**Features:**
+- Skip to main content, navigation, and footer
+- Landmark navigation menu
+- Keyboard shortcuts reference
+- Visual navigation helper
+
+**Usage:**
+```tsx
+import { AccessibilityNavigator } from '@/app/components/accessibility/AccessibilityNavigator';
+
+
+```
+
+### 2. ScreenReaderOptimizer
+Optimizes content for screen readers with ARIA labels and live regions.
+
+**Features:**
+- Automatic screen reader detection
+- ARIA live regions for announcements
+- Accessible loading states
+- Accessible error/success messages
+- Progress indicators with proper ARIA attributes
+
+**Usage:**
+```tsx
+import {
+ ScreenReaderOptimizer,
+ AccessibleLoading,
+ AccessibleError,
+ AccessibleProgress
+} from '@/app/components/accessibility/ScreenReaderOptimizer';
+
+
+
+
+
+// Loading state
+
+
+// Error message
+
{}} />
+
+// Progress bar
+
+```
+
+### 3. ColorContrastChecker
+Validates color contrast ratios against WCAG standards.
+
+**Features:**
+- Automatic page contrast checking
+- Visual contrast ratio display
+- WCAG AA/AAA compliance indicators
+- Color swatch previews
+- Detailed failure reports
+
+**Usage:**
+```tsx
+import { ColorContrastChecker } from '@/app/components/accessibility/ColorContrastChecker';
+
+
+```
+
+### 4. AccessibilityTester
+Automated accessibility testing and issue reporting.
+
+**Features:**
+- Comprehensive accessibility checks
+- Issue severity classification (critical, serious, moderate, minor)
+- WCAG criteria mapping
+- Exportable JSON reports
+- Real-time issue filtering
+
+**Usage:**
+```tsx
+import { AccessibilityTester } from '@/app/components/accessibility/AccessibilityTester';
+
+
+```
+
+### 5. AccessibilityProvider
+Wrapper component that enables all accessibility features.
+
+**Usage:**
+```tsx
+import { AccessibilityProvider } from '@/app/components/accessibility/AccessibilityProvider';
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+## Hooks
+
+### useKeyboardNavigation
+Manages keyboard navigation within a container.
+
+```tsx
+import { useKeyboardNavigation } from '@/hooks/useAccessibility';
+
+const containerRef = useKeyboardNavigation(true);
+return {/* content */}
;
+```
+
+### useFocusTrap
+Traps focus within a container (for modals/dialogs).
+
+```tsx
+import { useFocusTrap } from '@/hooks/useAccessibility';
+
+const containerRef = useFocusTrap(isModalOpen);
+return {/* modal content */}
;
+```
+
+### useScreenReaderAnnouncement
+Announces messages to screen readers.
+
+```tsx
+import { useScreenReaderAnnouncement } from '@/hooks/useAccessibility';
+
+const announce = useScreenReaderAnnouncement();
+announce('Form submitted successfully', 'polite');
+```
+
+### useAccessibilityCheck
+Performs accessibility checks on a container.
+
+```tsx
+import { useAccessibilityCheck } from '@/hooks/useAccessibility';
+
+const { containerRef, issues, checkAccessibility } = useAccessibilityCheck(false);
+```
+
+### useReducedMotion
+Detects user's reduced motion preference.
+
+```tsx
+import { useReducedMotion } from '@/hooks/useAccessibility';
+
+const prefersReducedMotion = useReducedMotion();
+const animationDuration = prefersReducedMotion ? 0 : 300;
+```
+
+## Utility Functions
+
+### calculateContrastRatio
+Calculates contrast ratio between two colors.
+
+```tsx
+import { calculateContrastRatio } from '@/utils/accessibilityUtils';
+
+const result = calculateContrastRatio('#000000', '#FFFFFF');
+console.log(result.ratio); // 21
+console.log(result.passes.aa); // true
+```
+
+### checkAccessibilityIssues
+Checks for common accessibility issues in a container.
+
+```tsx
+import { checkAccessibilityIssues } from '@/utils/accessibilityUtils';
+
+const issues = checkAccessibilityIssues(document.body);
+issues.forEach(issue => {
+ console.log(issue.severity, issue.message);
+});
+```
+
+### announceToScreenReader
+Announces a message to screen readers.
+
+```tsx
+import { announceToScreenReader } from '@/utils/accessibilityUtils';
+
+announceToScreenReader('Page loaded', 'polite');
+```
+
+## WCAG 2.1 AA Compliance Checklist
+
+### ✅ Perceivable
+- [x] 1.1.1 Non-text Content - Alt text checking
+- [x] 1.3.1 Info and Relationships - Semantic HTML and ARIA
+- [x] 1.4.3 Contrast (Minimum) - Color contrast checker
+- [x] 1.4.11 Non-text Contrast - UI component contrast
+
+### ✅ Operable
+- [x] 2.1.1 Keyboard - Full keyboard navigation
+- [x] 2.1.2 No Keyboard Trap - Focus trap management
+- [x] 2.4.1 Bypass Blocks - Skip links
+- [x] 2.4.3 Focus Order - Logical tab order
+- [x] 2.4.7 Focus Visible - Visible focus indicators
+
+### ✅ Understandable
+- [x] 3.1.1 Language of Page - Lang attributes
+- [x] 3.2.1 On Focus - No unexpected changes
+- [x] 3.3.1 Error Identification - Accessible errors
+- [x] 3.3.2 Labels or Instructions - Form labels
+
+### ✅ Robust
+- [x] 4.1.2 Name, Role, Value - ARIA attributes
+- [x] 4.1.3 Status Messages - Live regions
+
+## Testing
+
+### Manual Testing
+1. **Keyboard Navigation**: Navigate entire site using only Tab, Shift+Tab, Enter, and Arrow keys
+2. **Screen Reader**: Test with NVDA (Windows), JAWS (Windows), or VoiceOver (Mac)
+3. **Zoom**: Test at 200% zoom level
+4. **Color Blindness**: Use browser extensions to simulate color blindness
+
+### Automated Testing
+```tsx
+// Run accessibility check
+
+
+// Or programmatically
+const { issues, checkAccessibility } = useAccessibilityCheck();
+checkAccessibility();
+console.log(issues);
+```
+
+## Best Practices
+
+1. **Always provide alt text** for images (use empty alt="" for decorative images)
+2. **Use semantic HTML** (header, nav, main, footer, article, section)
+3. **Provide labels** for all form inputs
+4. **Maintain heading hierarchy** (don't skip levels)
+5. **Ensure sufficient color contrast** (4.5:1 for normal text, 3:1 for large text)
+6. **Make all functionality keyboard accessible**
+7. **Provide skip links** for keyboard users
+8. **Use ARIA attributes** appropriately (but prefer semantic HTML)
+9. **Test with real assistive technologies**
+10. **Include focus indicators** for all interactive elements
+
+## Browser Support
+
+- Chrome/Edge 90+
+- Firefox 88+
+- Safari 14+
+- Screen Readers: NVDA, JAWS, VoiceOver, TalkBack
+
+## Resources
+
+- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
+- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
+- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
+- [axe DevTools](https://www.deque.com/axe/devtools/)
+
+## Important Note
+
+While these tools provide comprehensive automated accessibility checking, they cannot catch all accessibility issues. Manual testing with assistive technologies and real users with disabilities is essential for true accessibility compliance.
+
+**This implementation does not guarantee WCAG compliance** - it provides tools to help achieve and maintain compliance through ongoing testing and remediation.
diff --git a/src/app/components/accessibility/ScreenReaderOptimizer.tsx b/src/app/components/accessibility/ScreenReaderOptimizer.tsx
new file mode 100644
index 0000000..b1dc78a
--- /dev/null
+++ b/src/app/components/accessibility/ScreenReaderOptimizer.tsx
@@ -0,0 +1,269 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+import { useAriaLive, useScreenReaderAnnouncement } from '@/hooks/useAccessibility';
+import { Volume2, VolumeX } from 'lucide-react';
+
+interface ScreenReaderOptimizerProps {
+ children: React.ReactNode;
+ enableAnnouncements?: boolean;
+}
+
+export function ScreenReaderOptimizer({
+ children,
+ enableAnnouncements = true,
+}: ScreenReaderOptimizerProps) {
+ const { LiveRegion } = useAriaLive();
+ const [isScreenReaderActive, setIsScreenReaderActive] = useState(false);
+
+ useEffect(() => {
+ // Detect if screen reader is likely active
+ const detectScreenReader = () => {
+ // Check for common screen reader indicators
+ const hasAriaLive = document.querySelectorAll('[aria-live]').length > 0;
+ const hasScreenReaderClass = document.body.classList.contains('screen-reader-active');
+
+ setIsScreenReaderActive(hasAriaLive || hasScreenReaderClass);
+ };
+
+ detectScreenReader();
+
+ // Monitor DOM changes
+ const observer = new MutationObserver(detectScreenReader);
+ observer.observe(document.body, {
+ attributes: true,
+ attributeFilter: ['class'],
+ });
+
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+ <>
+ {/* Screen Reader Only Content */}
+
+
Accessible Learning Platform
+
+ This platform is optimized for screen readers. Use heading navigation to jump between
+ sections, and form labels are provided for all inputs.
+
+
+
+ {/* Live Region for Announcements */}
+ {enableAnnouncements && }
+
+ {/* Main Content with Enhanced ARIA */}
+
+ {children}
+
+
+ {/* Screen Reader Status Indicator (Visual Only) */}
+
+ {isScreenReaderActive ? (
+ <>
+
+ SR Active
+ >
+ ) : (
+ <>
+
+ SR Inactive
+ >
+ )}
+
+ >
+ );
+}
+
+/**
+ * Component for creating accessible descriptions
+ */
+interface AccessibleDescriptionProps {
+ id: string;
+ children: React.ReactNode;
+}
+
+export function AccessibleDescription({ id, children }: AccessibleDescriptionProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Component for accessible loading states
+ */
+interface AccessibleLoadingProps {
+ message?: string;
+ isLoading: boolean;
+}
+
+export function AccessibleLoading({
+ message = 'Loading content',
+ isLoading,
+}: AccessibleLoadingProps) {
+ const announce = useScreenReaderAnnouncement();
+
+ useEffect(() => {
+ if (isLoading) {
+ announce(message, 'polite');
+ } else {
+ announce('Content loaded', 'polite');
+ }
+ }, [isLoading, message, announce]);
+
+ if (!isLoading) return null;
+
+ return (
+
+ );
+}
+
+/**
+ * Component for accessible error messages
+ */
+interface AccessibleErrorProps {
+ id?: string;
+ message: string;
+ onDismiss?: () => void;
+}
+
+export function AccessibleError({ id, message, onDismiss }: AccessibleErrorProps) {
+ const announce = useScreenReaderAnnouncement();
+
+ useEffect(() => {
+ announce(`Error: ${message}`, 'assertive');
+ }, [message, announce]);
+
+ return (
+
+
+
+ {onDismiss && (
+
+ )}
+
+
+ );
+}
+
+/**
+ * Component for accessible success messages
+ */
+interface AccessibleSuccessProps {
+ message: string;
+ onDismiss?: () => void;
+}
+
+export function AccessibleSuccess({ message, onDismiss }: AccessibleSuccessProps) {
+ const announce = useScreenReaderAnnouncement();
+
+ useEffect(() => {
+ announce(message, 'polite');
+ }, [message, announce]);
+
+ return (
+
+
+
+ {onDismiss && (
+
+ )}
+
+
+ );
+}
+
+/**
+ * Component for accessible progress indicators
+ */
+interface AccessibleProgressProps {
+ value: number;
+ max?: number;
+ label: string;
+ showPercentage?: boolean;
+}
+
+export function AccessibleProgress({
+ value,
+ max = 100,
+ label,
+ showPercentage = true,
+}: AccessibleProgressProps) {
+ const percentage = Math.round((value / max) * 100);
+
+ return (
+
+
+
+ {showPercentage && (
+
+ {percentage}%
+
+ )}
+
+
+
{`${label}: ${percentage}% complete`}
+
+ );
+}
diff --git a/src/app/components/accessibility/__tests__/accessibilityUtils.test.ts b/src/app/components/accessibility/__tests__/accessibilityUtils.test.ts
new file mode 100644
index 0000000..1824ae8
--- /dev/null
+++ b/src/app/components/accessibility/__tests__/accessibilityUtils.test.ts
@@ -0,0 +1,175 @@
+import { describe, it, expect } from 'vitest';
+import {
+ calculateContrastRatio,
+ isFocusable,
+ hasAccessibleName,
+ generateAriaId,
+ getWCAGLevel,
+ AccessibilityIssue,
+} from '@/utils/accessibilityUtils';
+
+describe('accessibilityUtils', () => {
+ describe('calculateContrastRatio', () => {
+ it('should calculate correct contrast ratio for black and white', () => {
+ const result = calculateContrastRatio('#000000', '#FFFFFF');
+ expect(result.ratio).toBe(21);
+ expect(result.passes.aa).toBe(true);
+ expect(result.passes.aaa).toBe(true);
+ });
+
+ it('should calculate correct contrast ratio for similar colors', () => {
+ const result = calculateContrastRatio('#777777', '#888888');
+ expect(result.ratio).toBeLessThan(4.5);
+ expect(result.passes.aa).toBe(false);
+ });
+
+ it('should pass AA for sufficient contrast', () => {
+ const result = calculateContrastRatio('#595959', '#FFFFFF');
+ expect(result.passes.aa).toBe(true);
+ });
+
+ it('should pass AA Large for lower contrast', () => {
+ const result = calculateContrastRatio('#767676', '#FFFFFF');
+ expect(result.passes.aaLarge).toBe(true);
+ });
+
+ it('should handle invalid hex colors', () => {
+ const result = calculateContrastRatio('invalid', '#FFFFFF');
+ expect(result.ratio).toBe(0);
+ expect(result.passes.aa).toBe(false);
+ });
+ });
+
+ describe('isFocusable', () => {
+ it('should identify button as focusable', () => {
+ const button = document.createElement('button');
+ expect(isFocusable(button)).toBe(true);
+ });
+
+ it('should identify link as focusable', () => {
+ const link = document.createElement('a');
+ link.href = '#';
+ expect(isFocusable(link)).toBe(true);
+ });
+
+ it('should identify element with tabindex as focusable', () => {
+ const div = document.createElement('div');
+ div.setAttribute('tabindex', '0');
+ expect(isFocusable(div)).toBe(true);
+ });
+
+ it('should not identify regular div as focusable', () => {
+ const div = document.createElement('div');
+ expect(isFocusable(div)).toBe(false);
+ });
+ });
+
+ describe('hasAccessibleName', () => {
+ it('should detect aria-label', () => {
+ const element = document.createElement('button');
+ element.setAttribute('aria-label', 'Close');
+ expect(hasAccessibleName(element)).toBe(true);
+ });
+
+ it('should detect text content', () => {
+ const element = document.createElement('button');
+ element.textContent = 'Click me';
+ expect(hasAccessibleName(element)).toBe(true);
+ });
+
+ it('should detect title attribute', () => {
+ const element = document.createElement('button');
+ element.setAttribute('title', 'Close button');
+ expect(hasAccessibleName(element)).toBe(true);
+ });
+
+ it('should return false for element without accessible name', () => {
+ const element = document.createElement('button');
+ expect(hasAccessibleName(element)).toBe(false);
+ });
+ });
+
+ describe('generateAriaId', () => {
+ it('should generate unique IDs', () => {
+ const id1 = generateAriaId();
+ const id2 = generateAriaId();
+ expect(id1).not.toBe(id2);
+ });
+
+ it('should use provided prefix', () => {
+ const id = generateAriaId('test');
+ expect(id).toMatch(/^test-/);
+ });
+
+ it('should use default prefix', () => {
+ const id = generateAriaId();
+ expect(id).toMatch(/^aria-/);
+ });
+ });
+
+ describe('getWCAGLevel', () => {
+ it('should return Fail for critical issues', () => {
+ const issues: AccessibilityIssue[] = [
+ {
+ id: '1',
+ severity: 'critical',
+ type: 'missing-alt',
+ element: 'img',
+ message: 'Missing alt',
+ wcagCriteria: ['1.1.1'],
+ suggestion: 'Add alt text',
+ },
+ ];
+ expect(getWCAGLevel(issues)).toBe('Fail');
+ });
+
+ it('should return A for serious issues', () => {
+ const issues: AccessibilityIssue[] = [
+ {
+ id: '1',
+ severity: 'serious',
+ type: 'missing-label',
+ element: 'input',
+ message: 'Missing label',
+ wcagCriteria: ['1.3.1'],
+ suggestion: 'Add label',
+ },
+ ];
+ expect(getWCAGLevel(issues)).toBe('A');
+ });
+
+ it('should return AA for moderate issues', () => {
+ const issues: AccessibilityIssue[] = Array(6)
+ .fill(null)
+ .map((_, i) => ({
+ id: `${i}`,
+ severity: 'moderate' as const,
+ type: 'heading',
+ element: 'h2',
+ message: 'Issue',
+ wcagCriteria: ['1.3.1'],
+ suggestion: 'Fix',
+ }));
+ expect(getWCAGLevel(issues)).toBe('AA');
+ });
+
+ it('should return AAA for few minor issues', () => {
+ const issues: AccessibilityIssue[] = [
+ {
+ id: '1',
+ severity: 'minor',
+ type: 'minor-issue',
+ element: 'div',
+ message: 'Minor issue',
+ wcagCriteria: ['2.4.4'],
+ suggestion: 'Improve',
+ },
+ ];
+ expect(getWCAGLevel(issues)).toBe('AAA');
+ });
+
+ it('should return AAA for no issues', () => {
+ expect(getWCAGLevel([])).toBe('AAA');
+ });
+ });
+});
diff --git a/src/app/components/accessibility/examples/AccessibleFormExample.tsx b/src/app/components/accessibility/examples/AccessibleFormExample.tsx
new file mode 100644
index 0000000..5dece39
--- /dev/null
+++ b/src/app/components/accessibility/examples/AccessibleFormExample.tsx
@@ -0,0 +1,175 @@
+'use client';
+
+import React, { useState } from 'react';
+import { useScreenReaderAnnouncement, useFocusTrap } from '@/hooks/useAccessibility';
+import { AccessibleError, AccessibleSuccess } from '../ScreenReaderOptimizer';
+
+/**
+ * Example of an accessible form with proper ARIA labels,
+ * error handling, and screen reader announcements
+ */
+export function AccessibleFormExample() {
+ const [formData, setFormData] = useState({ name: '', email: '', message: '' });
+ const [errors, setErrors] = useState>({});
+ const [success, setSuccess] = useState(false);
+ const announce = useScreenReaderAnnouncement();
+
+ const validateForm = () => {
+ const newErrors: Record = {};
+
+ if (!formData.name.trim()) {
+ newErrors.name = 'Name is required';
+ }
+
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = 'Please enter a valid email address';
+ }
+
+ if (!formData.message.trim()) {
+ newErrors.message = 'Message is required';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (validateForm()) {
+ setSuccess(true);
+ announce('Form submitted successfully', 'polite');
+ // Reset form
+ setFormData({ name: '', email: '', message: '' });
+ setTimeout(() => setSuccess(false), 5000);
+ } else {
+ announce(`Form has ${Object.keys(errors).length} errors. Please correct them.`, 'assertive');
+ }
+ };
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ // Clear error when user starts typing
+ if (errors[field]) {
+ setErrors((prev) => {
+ const newErrors = { ...prev };
+ delete newErrors[field];
+ return newErrors;
+ });
+ }
+ };
+
+ return (
+
+
Accessible Contact Form
+
+ {success && (
+
+
setSuccess(false)}
+ />
+
+ )}
+
+
+
+ {/* Form Instructions (Screen Reader) */}
+
+
All fields marked with an asterisk are required.
+
Press Tab to move between fields. Press Enter to submit the form.
+
+
+ );
+}
diff --git a/src/app/components/accessibility/examples/AccessibleModalExample.tsx b/src/app/components/accessibility/examples/AccessibleModalExample.tsx
new file mode 100644
index 0000000..686142f
--- /dev/null
+++ b/src/app/components/accessibility/examples/AccessibleModalExample.tsx
@@ -0,0 +1,144 @@
+'use client';
+
+import React, { useEffect } from 'react';
+import { useFocusTrap, useScreenReaderAnnouncement } from '@/hooks/useAccessibility';
+import { X } from 'lucide-react';
+
+interface AccessibleModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ children: React.ReactNode;
+}
+
+/**
+ * Example of an accessible modal dialog with focus trap,
+ * keyboard navigation, and proper ARIA attributes
+ */
+export function AccessibleModalExample({
+ isOpen,
+ onClose,
+ title,
+ children,
+}: AccessibleModalProps) {
+ const containerRef = useFocusTrap(isOpen);
+ const announce = useScreenReaderAnnouncement();
+
+ useEffect(() => {
+ if (isOpen) {
+ announce(`${title} dialog opened`, 'polite');
+ // Prevent body scroll
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = '';
+ }
+
+ return () => {
+ document.body.style.overflow = '';
+ };
+ }, [isOpen, title, announce]);
+
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && isOpen) {
+ onClose();
+ }
+ };
+
+ document.addEventListener('keydown', handleEscape);
+ return () => document.removeEventListener('keydown', handleEscape);
+ }, [isOpen, onClose]);
+
+ if (!isOpen) return null;
+
+ return (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+
+ {/* Header */}
+
+
+ {title}
+
+
+
+
+ {/* Content */}
+
{children}
+
+ {/* Footer */}
+
+
+
+
+
+
+ >
+ );
+}
+
+/**
+ * Example usage component
+ */
+export function ModalExampleUsage() {
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ return (
+
+
Accessible Modal Example
+
+
+
setIsOpen(false)}
+ title="Example Dialog"
+ >
+
+ This is an accessible modal dialog with proper focus management and keyboard navigation.
+
+
+ - Focus is trapped within the modal
+ - Press Escape to close
+ - Tab cycles through focusable elements
+ - Screen readers announce the dialog
+ - Background is not scrollable
+
+
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 69e5d54..bd31467 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -32,6 +32,33 @@ body {
white-space: nowrap;
border-width: 0;
}
+
+/* Not screen reader only - visible when focused */
+.sr-only.focus\:not-sr-only:focus,
+.focus-within\:not-sr-only:focus-within {
+ position: static;
+ width: auto;
+ height: auto;
+ padding: inherit;
+ margin: inherit;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+}
+
+/* Focus visible styles for keyboard navigation */
+*:focus-visible {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+}
+
+/* Skip to content link styles */
+a[data-skip-link]:focus {
+ position: fixed;
+ top: 1rem;
+ left: 1rem;
+ z-index: 9999;
+}
@layer utilities {
.grid-bg {
background-size: 40px 40px;
diff --git a/src/app/layout-with-accessibility.tsx.example b/src/app/layout-with-accessibility.tsx.example
new file mode 100644
index 0000000..e1b4083
--- /dev/null
+++ b/src/app/layout-with-accessibility.tsx.example
@@ -0,0 +1,102 @@
+/**
+ * Example: How to integrate accessibility features into your root layout
+ *
+ * To use this:
+ * 1. Copy this content to your actual layout.tsx file
+ * 2. Adjust the imports based on your project structure
+ * 3. Customize the AccessibilityProvider props as needed
+ */
+
+import type { Metadata } from 'next';
+import { Inter } from 'next/font/google';
+import './globals.css';
+import { AccessibilityProvider } from '@/app/components/accessibility/AccessibilityProvider';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export const metadata: Metadata = {
+ title: 'Accessible Learning Platform',
+ description: 'WCAG 2.1 AA Compliant Learning Platform',
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {/* Skip Link - Always first in DOM */}
+
+ Skip to main content
+
+
+ {/* Your app content */}
+ {children}
+
+
+
+ );
+}
+
+/**
+ * PRODUCTION CONFIGURATION:
+ *
+ * For production, you might want to disable the visual widgets
+ * and only enable them in development or for specific user roles:
+ */
+
+// Example: Development only
+const isDevelopment = process.env.NODE_ENV === 'development';
+
+export function ProductionLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+/**
+ * MINIMAL CONFIGURATION:
+ *
+ * If you only want screen reader support without the visual widgets:
+ */
+
+export function MinimalAccessibilityLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/hooks/useAccessibility.tsx b/src/hooks/useAccessibility.tsx
new file mode 100644
index 0000000..503a7f8
--- /dev/null
+++ b/src/hooks/useAccessibility.tsx
@@ -0,0 +1,216 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+import {
+ getFocusableElements,
+ trapFocus,
+ announceToScreenReader,
+ checkAccessibilityIssues,
+ AccessibilityIssue,
+} from '@/utils/accessibilityUtils';
+
+/**
+ * Hook for managing keyboard navigation
+ */
+export function useKeyboardNavigation(enabled: boolean = true) {
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ if (!enabled || !containerRef.current) return;
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Skip links (Ctrl/Cmd + K)
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
+ event.preventDefault();
+ const skipLink = document.querySelector('[data-skip-link]');
+ skipLink?.focus();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [enabled]);
+
+ return containerRef;
+}
+
+/**
+ * Hook for focus trap (modals, dialogs)
+ */
+export function useFocusTrap(isActive: boolean = false) {
+ const containerRef = useRef(null);
+ const previousFocusRef = useRef(null);
+
+ useEffect(() => {
+ if (!isActive || !containerRef.current) return;
+
+ // Store previous focus
+ previousFocusRef.current = document.activeElement as HTMLElement;
+
+ // Focus first focusable element
+ const focusableElements = getFocusableElements(containerRef.current);
+ focusableElements[0]?.focus();
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (containerRef.current) {
+ trapFocus(containerRef.current, event);
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ // Restore previous focus
+ previousFocusRef.current?.focus();
+ };
+ }, [isActive]);
+
+ return containerRef;
+}
+
+/**
+ * Hook for managing focus visibility
+ */
+export function useFocusVisible() {
+ const [isFocusVisible, setIsFocusVisible] = useState(false);
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Tab') {
+ setIsFocusVisible(true);
+ }
+ };
+
+ const handleMouseDown = () => {
+ setIsFocusVisible(false);
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ document.addEventListener('mousedown', handleMouseDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ document.removeEventListener('mousedown', handleMouseDown);
+ };
+ }, []);
+
+ return isFocusVisible;
+}
+
+/**
+ * Hook for screen reader announcements
+ */
+export function useScreenReaderAnnouncement() {
+ const announce = useCallback(
+ (message: string, priority: 'polite' | 'assertive' = 'polite') => {
+ announceToScreenReader(message, priority);
+ },
+ []
+ );
+
+ return announce;
+}
+
+/**
+ * Hook for accessibility testing
+ */
+export function useAccessibilityCheck(autoCheck: boolean = false) {
+ const [issues, setIssues] = useState([]);
+ const [isChecking, setIsChecking] = useState(false);
+ const containerRef = useRef(null);
+
+ const checkAccessibility = useCallback(() => {
+ if (!containerRef.current) return;
+
+ setIsChecking(true);
+ const foundIssues = checkAccessibilityIssues(containerRef.current);
+ setIssues(foundIssues);
+ setIsChecking(false);
+ }, []);
+
+ useEffect(() => {
+ if (autoCheck && containerRef.current) {
+ // Delay check to allow DOM to settle
+ const timer = setTimeout(checkAccessibility, 500);
+ return () => clearTimeout(timer);
+ }
+ }, [autoCheck, checkAccessibility]);
+
+ return {
+ containerRef,
+ issues,
+ isChecking,
+ checkAccessibility,
+ };
+}
+
+/**
+ * Hook for managing ARIA live regions
+ */
+export function useAriaLive() {
+ const liveRegionRef = useRef(null);
+
+ const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
+ if (liveRegionRef.current) {
+ liveRegionRef.current.setAttribute('aria-live', priority);
+ liveRegionRef.current.textContent = message;
+
+ // Clear after announcement
+ setTimeout(() => {
+ if (liveRegionRef.current) {
+ liveRegionRef.current.textContent = '';
+ }
+ }, 1000);
+ }
+ }, []);
+
+ const LiveRegion = useCallback(
+ () => (
+
+ ),
+ []
+ );
+
+ return { announce, LiveRegion };
+}
+
+/**
+ * Hook for skip navigation
+ */
+export function useSkipNavigation() {
+ const skipToContent = useCallback((targetId: string) => {
+ const target = document.getElementById(targetId);
+ if (target) {
+ target.focus();
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ }, []);
+
+ return skipToContent;
+}
+
+/**
+ * Hook for reduced motion preference
+ */
+export function useReducedMotion() {
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+ setPrefersReducedMotion(mediaQuery.matches);
+
+ const handleChange = (event: MediaQueryListEvent) => {
+ setPrefersReducedMotion(event.matches);
+ };
+
+ mediaQuery.addEventListener('change', handleChange);
+ return () => mediaQuery.removeEventListener('change', handleChange);
+ }, []);
+
+ return prefersReducedMotion;
+}
diff --git a/src/utils/accessibilityUtils.ts b/src/utils/accessibilityUtils.ts
new file mode 100644
index 0000000..c4f4e90
--- /dev/null
+++ b/src/utils/accessibilityUtils.ts
@@ -0,0 +1,297 @@
+/**
+ * Accessibility utility functions for WCAG 2.1 AA compliance
+ */
+
+export interface ColorContrastResult {
+ ratio: number;
+ passes: {
+ aa: boolean;
+ aaa: boolean;
+ aaLarge: boolean;
+ aaaLarge: boolean;
+ };
+}
+
+export interface AccessibilityIssue {
+ id: string;
+ severity: 'critical' | 'serious' | 'moderate' | 'minor';
+ type: string;
+ element: string;
+ message: string;
+ wcagCriteria: string[];
+ suggestion: string;
+}
+
+/**
+ * Calculate relative luminance of a color
+ */
+function getLuminance(r: number, g: number, b: number): number {
+ const [rs, gs, bs] = [r, g, b].map((c) => {
+ const val = c / 255;
+ return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
+ });
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
+}
+
+/**
+ * Parse hex color to RGB
+ */
+function 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;
+}
+
+/**
+ * Calculate contrast ratio between two colors
+ */
+export function calculateContrastRatio(
+ foreground: string,
+ background: string
+): ColorContrastResult {
+ const fg = hexToRgb(foreground);
+ const bg = hexToRgb(background);
+
+ if (!fg || !bg) {
+ return {
+ ratio: 0,
+ passes: { aa: false, aaa: false, aaLarge: false, aaaLarge: false },
+ };
+ }
+
+ const l1 = getLuminance(fg.r, fg.g, fg.b);
+ const l2 = getLuminance(bg.r, bg.g, bg.b);
+ const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
+
+ return {
+ ratio: Math.round(ratio * 100) / 100,
+ passes: {
+ aa: ratio >= 4.5,
+ aaa: ratio >= 7,
+ aaLarge: ratio >= 3,
+ aaaLarge: ratio >= 4.5,
+ },
+ };
+}
+
+/**
+ * Get computed color from element
+ */
+export function getComputedColor(element: HTMLElement, property: string): string {
+ const color = window.getComputedStyle(element).getPropertyValue(property);
+ return rgbToHex(color);
+}
+
+/**
+ * Convert RGB to hex
+ */
+function rgbToHex(rgb: string): string {
+ const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
+ if (!match) return '#000000';
+
+ const hex = (x: string) => {
+ const val = parseInt(x).toString(16);
+ return val.length === 1 ? '0' + val : val;
+ };
+
+ return '#' + hex(match[1]) + hex(match[2]) + hex(match[3]);
+}
+
+/**
+ * Check if element is focusable
+ */
+export function isFocusable(element: HTMLElement): boolean {
+ const focusableTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
+ const tabIndex = element.getAttribute('tabindex');
+
+ return (
+ focusableTags.includes(element.tagName) ||
+ (tabIndex !== null && parseInt(tabIndex) >= 0) ||
+ element.hasAttribute('contenteditable')
+ );
+}
+
+/**
+ * Get all focusable elements within a container
+ */
+export function getFocusableElements(container: HTMLElement): HTMLElement[] {
+ const selector =
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable]';
+ return Array.from(container.querySelectorAll(selector)) as HTMLElement[];
+}
+
+/**
+ * Trap focus within a container
+ */
+export function trapFocus(container: HTMLElement, event: KeyboardEvent): void {
+ if (event.key !== 'Tab') return;
+
+ const focusableElements = getFocusableElements(container);
+ const firstElement = focusableElements[0];
+ const lastElement = focusableElements[focusableElements.length - 1];
+
+ if (event.shiftKey && document.activeElement === firstElement) {
+ event.preventDefault();
+ lastElement?.focus();
+ } else if (!event.shiftKey && document.activeElement === lastElement) {
+ event.preventDefault();
+ firstElement?.focus();
+ }
+}
+
+/**
+ * Check if element has accessible name
+ */
+export function hasAccessibleName(element: HTMLElement): boolean {
+ return !!(
+ element.getAttribute('aria-label') ||
+ element.getAttribute('aria-labelledby') ||
+ element.textContent?.trim() ||
+ element.getAttribute('title')
+ );
+}
+
+/**
+ * Generate unique ID for ARIA attributes
+ */
+export function generateAriaId(prefix: string = 'aria'): string {
+ return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
+}
+
+/**
+ * Announce message to screen readers
+ */
+export function announceToScreenReader(
+ message: string,
+ priority: 'polite' | 'assertive' = 'polite'
+): void {
+ const announcement = document.createElement('div');
+ announcement.setAttribute('role', 'status');
+ announcement.setAttribute('aria-live', priority);
+ announcement.setAttribute('aria-atomic', 'true');
+ announcement.className = 'sr-only';
+ announcement.textContent = message;
+
+ document.body.appendChild(announcement);
+
+ setTimeout(() => {
+ document.body.removeChild(announcement);
+ }, 1000);
+}
+
+/**
+ * Check for common accessibility issues
+ */
+export function checkAccessibilityIssues(
+ container: HTMLElement
+): AccessibilityIssue[] {
+ const issues: AccessibilityIssue[] = [];
+
+ // Check images for alt text
+ const images = container.querySelectorAll('img');
+ images.forEach((img, index) => {
+ if (!img.hasAttribute('alt')) {
+ issues.push({
+ id: `img-alt-${index}`,
+ severity: 'critical',
+ type: 'missing-alt',
+ element: 'img',
+ message: 'Image missing alt attribute',
+ wcagCriteria: ['1.1.1'],
+ suggestion: 'Add descriptive alt text or alt="" for decorative images',
+ });
+ }
+ });
+
+ // Check form inputs for labels
+ const inputs = container.querySelectorAll('input, select, textarea');
+ inputs.forEach((input, index) => {
+ const hasLabel =
+ input.hasAttribute('aria-label') ||
+ input.hasAttribute('aria-labelledby') ||
+ container.querySelector(`label[for="${input.id}"]`);
+
+ if (!hasLabel) {
+ issues.push({
+ id: `input-label-${index}`,
+ severity: 'critical',
+ type: 'missing-label',
+ element: input.tagName.toLowerCase(),
+ message: 'Form input missing accessible label',
+ wcagCriteria: ['1.3.1', '4.1.2'],
+ suggestion: 'Add aria-label or associate with a