Semantic HTML & ARIA
Build accessible React components with proper HTML semantics and ARIA attributes
Semantic HTML provides meaning to screen readers and assistive technologies. ARIA (Accessible Rich Internet Applications) attributes enhance accessibility when semantic HTML alone is insufficient.
Implementation
1// Semantic HTML and ARIA in React Components2import React, { useState, useId } from 'react';34// 1. Accessible Button Component5interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {6 variant?: 'primary' | 'secondary' | 'danger';7 loading?: boolean;8 icon?: React.ReactNode;9 children: React.ReactNode;10}1112const Button: React.FC<ButtonProps> = ({13 variant = 'primary',14 loading = false,15 disabled,16 icon,17 children,18 'aria-label': ariaLabel,19 ...props20}) => {21 // Ensure button has accessible name22 const accessibleName = ariaLabel || (typeof children === 'string' ? children : undefined);2324 return (25 <button26 {...props}27 disabled={disabled || loading}28 aria-label={accessibleName}29 aria-busy={loading}30 aria-disabled={disabled || loading}31 className={`btn btn-${variant} ${loading ? 'loading' : ''}`}32 >33 {loading ? (34 <>35 <span className="spinner" aria-hidden="true" />36 <span className="sr-only">Loading...</span>37 {children}38 </>39 ) : (40 <>41 {icon && <span aria-hidden="true">{icon}</span>}42 {children}43 </>44 )}45 </button>46 );47};4849// 2. Accessible Form with Labels and Error Messages50interface FormFieldProps {51 label: string;52 error?: string;53 required?: boolean;54 helpText?: string;55 children: (props: {56 id: string;57 'aria-invalid': boolean;58 'aria-describedby': string | undefined;59 'aria-required': boolean;60 }) => React.ReactNode;61}6263const FormField: React.FC<FormFieldProps> = ({64 label,65 error,66 required = false,67 helpText,68 children69}) => {70 const id = useId();71 const errorId = `${id}-error`;72 const helpId = `${id}-help`;7374 const ariaDescribedBy = [75 error && errorId,76 helpText && helpId77 ].filter(Boolean).join(' ') || undefined;7879 return (80 <div className="form-field">81 <label htmlFor={id} className="form-label">82 {label}83 {required && <span aria-label="required" className="required">*</span>}84 </label>8586 {children({87 id,88 'aria-invalid': !!error,89 'aria-describedby': ariaDescribedBy,90 'aria-required': required91 })}9293 {helpText && (94 <p id={helpId} className="help-text">95 {helpText}96 </p>97 )}9899 {error && (100 <p id={errorId} className="error-message" role="alert">101 <span className="sr-only">Error:</span> {error}102 </p>103 )}104 </div>105 );106};107108// 3. Accessible Navigation with Landmarks109const Navigation: React.FC = () => {110 const [mobileMenuOpen, setMobileMenuOpen] = useState(false);111112 return (113 <nav aria-label="Main navigation">114 <button115 aria-expanded={mobileMenuOpen}116 aria-controls="mobile-menu"117 aria-label="Toggle navigation menu"118 onClick={() => setMobileMenuOpen(!mobileMenuOpen)}119 className="mobile-menu-toggle"120 >121 <span className="hamburger" aria-hidden="true" />122 </button>123124 <ul125 id="mobile-menu"126 className={`nav-menu ${mobileMenuOpen ? 'open' : ''}`}127 >128 <li><a href="/" aria-current="page">Home</a></li>129 <li><a href="/about">About</a></li>130 <li>131 <button132 aria-haspopup="true"133 aria-expanded="false"134 aria-controls="products-menu"135 >136 Products137 </button>138 <ul id="products-menu" role="menu">139 <li role="menuitem"><a href="/products/software">Software</a></li>140 <li role="menuitem"><a href="/products/hardware">Hardware</a></li>141 </ul>142 </li>143 </ul>144 </nav>145 );146};147148// 4. Accessible Modal/Dialog149interface ModalProps {150 isOpen: boolean;151 onClose: () => void;152 title: string;153 children: React.ReactNode;154}155156const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }) => {157 const titleId = useId();158159 useEffect(() => {160 if (isOpen) {161 // Save last focused element162 const lastFocused = document.activeElement as HTMLElement;163164 // Focus first focusable element in modal165 const modal = document.getElementById('modal');166 const focusable = modal?.querySelector<HTMLElement>(167 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'168 );169 focusable?.focus();170171 // Trap focus within modal172 const handleTab = (e: KeyboardEvent) => {173 if (e.key !== 'Tab') return;174175 const focusableElements = modal?.querySelectorAll<HTMLElement>(176 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'177 );178179 if (!focusableElements?.length) return;180181 const firstElement = focusableElements[0];182 const lastElement = focusableElements[focusableElements.length - 1];183184 if (e.shiftKey && document.activeElement === firstElement) {185 e.preventDefault();186 lastElement.focus();187 } else if (!e.shiftKey && document.activeElement === lastElement) {188 e.preventDefault();189 firstElement.focus();190 }191 };192193 document.addEventListener('keydown', handleTab);194195 return () => {196 document.removeEventListener('keydown', handleTab);197 lastFocused?.focus();198 };199 }200 }, [isOpen]);201202 if (!isOpen) return null;203204 return (205 <div206 role="dialog"207 aria-modal="true"208 aria-labelledby={titleId}209 id="modal"210 className="modal-overlay"211 onClick={(e) => {212 if (e.target === e.currentTarget) onClose();213 }}214 >215 <div className="modal-content">216 <h2 id={titleId}>{title}</h2>217 <button218 onClick={onClose}219 aria-label="Close dialog"220 className="close-button"221 >222 ×223 </button>224 {children}225 </div>226 </div>227 );228};
Advanced Patterns
1// Advanced ARIA Patterns2// 1. Accessible Tabs Component3interface TabsProps {4 tabs: Array<{ id: string; label: string; content: React.ReactNode }>;5 defaultTab?: string;6}78const Tabs: React.FC<TabsProps> = ({ tabs, defaultTab }) => {9 const [activeTab, setActiveTab] = useState(defaultTab || tabs[0]?.id);10 const tablistRef = useRef<HTMLDivElement>(null);1112 const handleKeyDown = (e: React.KeyboardEvent, currentIndex: number) => {13 const tabButtons = tablistRef.current?.querySelectorAll('[role="tab"]');14 if (!tabButtons) return;1516 let newIndex = currentIndex;1718 switch (e.key) {19 case 'ArrowRight':20 newIndex = (currentIndex + 1) % tabs.length;21 break;22 case 'ArrowLeft':23 newIndex = (currentIndex - 1 + tabs.length) % tabs.length;24 break;25 case 'Home':26 newIndex = 0;27 break;28 case 'End':29 newIndex = tabs.length - 1;30 break;31 default:32 return;33 }3435 e.preventDefault();36 const newTab = tabs[newIndex];37 setActiveTab(newTab.id);38 (tabButtons[newIndex] as HTMLElement).focus();39 };4041 return (42 <div className="tabs">43 <div44 ref={tablistRef}45 role="tablist"46 aria-label="Content tabs"47 className="tab-list"48 >49 {tabs.map((tab, index) => (50 <button51 key={tab.id}52 role="tab"53 id={`tab-${tab.id}`}54 aria-selected={activeTab === tab.id}55 aria-controls={`panel-${tab.id}`}56 tabIndex={activeTab === tab.id ? 0 : -1}57 onClick={() => setActiveTab(tab.id)}58 onKeyDown={(e) => handleKeyDown(e, index)}59 className={`tab-button ${activeTab === tab.id ? 'active' : ''}`}60 >61 {tab.label}62 </button>63 ))}64 </div>6566 {tabs.map(tab => (67 <div68 key={tab.id}69 role="tabpanel"70 id={`panel-${tab.id}`}71 aria-labelledby={`tab-${tab.id}`}72 hidden={activeTab !== tab.id}73 tabIndex={0}74 className="tab-panel"75 >76 {tab.content}77 </div>78 ))}79 </div>80 );81};8283// 2. Live Region for Dynamic Updates84const LiveAnnouncements: React.FC = () => {85 const [message, setMessage] = useState('');8687 const announce = (text: string, priority: 'polite' | 'assertive' = 'polite') => {88 setMessage(''); // Clear first to ensure announcement89 setTimeout(() => setMessage(text), 100);90 };9192 return (93 <>94 <div95 role="status"96 aria-live="polite"97 aria-atomic="true"98 className="sr-only"99 >100 {message}101 </div>102103 <button onClick={() => announce('Item added to cart')}>104 Add to Cart105 </button>106107 <button onClick={() => announce('Error: Invalid input', 'assertive')}>108 Submit Form109 </button>110 </>111 );112};113114// 3. Accessible Data Table115interface TableData {116 headers: string[];117 rows: Array<Record<string, any>>;118 caption?: string;119}120121const AccessibleTable: React.FC<TableData> = ({ headers, rows, caption }) => {122 const [sortColumn, setSortColumn] = useState<string | null>(null);123 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');124125 const handleSort = (column: string) => {126 if (sortColumn === column) {127 setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');128 } else {129 setSortColumn(column);130 setSortDirection('asc');131 }132 };133134 return (135 <table role="table" aria-label={caption}>136 {caption && <caption>{caption}</caption>}137 <thead>138 <tr role="row">139 {headers.map(header => (140 <th141 key={header}142 role="columnheader"143 aria-sort={144 sortColumn === header145 ? sortDirection === 'asc' ? 'ascending' : 'descending'146 : 'none'147 }148 >149 <button150 onClick={() => handleSort(header)}151 aria-label={`Sort by ${header} ${152 sortColumn === header153 ? sortDirection === 'asc' ? 'descending' : 'ascending'154 : ''155 }`}156 >157 {header}158 <span aria-hidden="true">159 {sortColumn === header && (160 sortDirection === 'asc' ? ' ↑' : ' ↓'161 )}162 </span>163 </button>164 </th>165 ))}166 </tr>167 </thead>168 <tbody>169 {rows.map((row, index) => (170 <tr key={index} role="row">171 {headers.map(header => (172 <td key={header} role="cell">173 {row[header]}174 </td>175 ))}176 </tr>177 ))}178 </tbody>179 </table>180 );181};