11.2 נגישות מתקדמת פתרון
פתרון - נגישות מתקדמת¶
פתרון תרגיל 1 - טופס הרשמה נגיש¶
'use client';
import { useState, useRef, useEffect, type FormEvent } from 'react';
interface FormErrors {
name?: string;
email?: string;
password?: string;
confirmPassword?: string;
gender?: string;
terms?: string;
}
function AccessibleRegistrationForm() {
const [errors, setErrors] = useState<FormErrors>({});
const [submitted, setSubmitted] = useState(false);
const errorSummaryRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (Object.keys(errors).length > 0 && submitted) {
errorSummaryRef.current?.focus();
}
}, [errors, submitted]);
function validate(formData: FormData): FormErrors {
const newErrors: FormErrors = {};
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const confirmPassword = formData.get('confirmPassword') as string;
const gender = formData.get('gender') as string;
const terms = formData.get('terms');
if (!name || name.length < 2) {
newErrors.name = 'שם מלא חייב להכיל לפחות 2 תווים';
}
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
newErrors.email = 'כתובת אימייל לא תקינה';
}
if (!password || password.length < 8) {
newErrors.password = 'סיסמה חייבת להכיל לפחות 8 תווים';
}
if (password !== confirmPassword) {
newErrors.confirmPassword = 'הסיסמאות לא תואמות';
}
if (!gender) {
newErrors.gender = 'יש לבחור מגדר';
}
if (!terms) {
newErrors.terms = 'יש לאשר את תנאי השימוש';
}
return newErrors;
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setSubmitted(true);
const formData = new FormData(e.currentTarget);
const newErrors = validate(formData);
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
// שליחת הטופס
console.log('הטופס תקין!');
}
}
const errorEntries = Object.entries(errors) as [keyof FormErrors, string][];
return (
<form onSubmit={handleSubmit} aria-label="טופס הרשמה" noValidate>
{/* סיכום שגיאות */}
{errorEntries.length > 0 && submitted && (
<div
ref={errorSummaryRef}
tabIndex={-1}
role="alert"
aria-label="שגיאות בטופס"
className="error-summary"
>
<h2>נמצאו {errorEntries.length} שגיאות:</h2>
<ul>
{errorEntries.map(([field, message]) => (
<li key={field}>
<a href={`#${field}`}>{message}</a>
</li>
))}
</ul>
</div>
)}
{/* שם מלא */}
<div>
<label htmlFor="name">שם מלא</label>
<input
id="name"
name="name"
type="text"
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<p id="name-error" className="error" role="alert">
{errors.name}
</p>
)}
</div>
{/* אימייל */}
<div>
<label htmlFor="email">אימייל</label>
<input
id="email"
name="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" className="error" role="alert">
{errors.email}
</p>
)}
</div>
{/* סיסמה */}
<div>
<label htmlFor="password">סיסמה</label>
<input
id="password"
name="password"
type="password"
aria-required="true"
aria-invalid={!!errors.password}
aria-describedby="password-help password-error"
/>
<p id="password-help" className="help-text">
לפחות 8 תווים, כולל אות גדולה, מספר ותו מיוחד
</p>
{errors.password && (
<p id="password-error" className="error" role="alert">
{errors.password}
</p>
)}
</div>
{/* אישור סיסמה */}
<div>
<label htmlFor="confirmPassword">אישור סיסמה</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
aria-required="true"
aria-invalid={!!errors.confirmPassword}
aria-describedby={
errors.confirmPassword ? 'confirm-error' : undefined
}
/>
{errors.confirmPassword && (
<p id="confirm-error" className="error" role="alert">
{errors.confirmPassword}
</p>
)}
</div>
{/* מגדר */}
<fieldset aria-invalid={!!errors.gender}>
<legend>מגדר</legend>
<div>
<input type="radio" id="gender-male" name="gender" value="male" />
<label htmlFor="gender-male">זכר</label>
</div>
<div>
<input type="radio" id="gender-female" name="gender" value="female" />
<label htmlFor="gender-female">נקבה</label>
</div>
<div>
<input type="radio" id="gender-other" name="gender" value="other" />
<label htmlFor="gender-other">אחר</label>
</div>
{errors.gender && (
<p className="error" role="alert">{errors.gender}</p>
)}
</fieldset>
{/* תנאי שימוש */}
<div>
<input
type="checkbox"
id="terms"
name="terms"
aria-invalid={!!errors.terms}
aria-describedby={errors.terms ? 'terms-error' : undefined}
/>
<label htmlFor="terms">
אני מסכים/ה <a href="/terms">לתנאי השימוש</a>
</label>
{errors.terms && (
<p id="terms-error" className="error" role="alert">
{errors.terms}
</p>
)}
</div>
<button type="submit">הרשמה</button>
</form>
);
}
export default AccessibleRegistrationForm;
פתרון תרגיל 2 - מודל נגיש עם מלכודת פוקוס¶
'use client';
import { useEffect, useRef, useCallback, type ReactNode } from 'react';
// קומפוננטת מלכודת פוקוס
function FocusTrap({ children, active }: { children: ReactNode; active: boolean }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!active) return;
const container = containerRef.current;
if (!container) return;
const getFocusableElements = () =>
container.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const elements = getFocusableElements();
const first = elements[0];
const last = elements[elements.length - 1];
first?.focus();
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
const currentElements = getFocusableElements();
const firstEl = currentElements[0];
const lastEl = currentElements[currentElements.length - 1];
if (e.shiftKey && document.activeElement === firstEl) {
e.preventDefault();
lastEl?.focus();
} else if (!e.shiftKey && document.activeElement === lastEl) {
e.preventDefault();
firstEl?.focus();
}
}
container.addEventListener('keydown', handleKeyDown);
return () => container.removeEventListener('keydown', handleKeyDown);
}, [active]);
return <div ref={containerRef}>{children}</div>;
}
// קומפוננטת מודל נגיש
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
}
function AccessibleModal({ isOpen, onClose, title, children }: ModalProps) {
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement;
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
previousFocusRef.current?.focus();
};
}, [isOpen]);
const handleEscape = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
},
[onClose]
);
useEffect(() => {
if (!isOpen) return;
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, handleEscape]);
if (!isOpen) return null;
return (
<>
{/* שכבת רקע */}
<div
className="modal-overlay"
onClick={onClose}
aria-hidden="true"
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 999,
}}
/>
{/* המודל */}
<FocusTrap active={isOpen}>
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '24px',
borderRadius: '8px',
zIndex: 1000,
minWidth: '300px',
}}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="סגור חלון">
X
</button>
</div>
</FocusTrap>
</>
);
}
// דוגמת שימוש - מודל אישור מחיקה
function DeleteConfirmation() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>מחק פריט</button>
<AccessibleModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="אישור מחיקה"
>
<p>האם אתה בטוח שברצונך למחוק את הפריט? לא ניתן לבטל פעולה זו.</p>
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<button
onClick={() => {
// ביצוע מחיקה
setIsOpen(false);
}}
>
אישור מחיקה
</button>
<button onClick={() => setIsOpen(false)}>ביטול</button>
</div>
</AccessibleModal>
</>
);
}
import { useState } from 'react';
export { AccessibleModal, FocusTrap, DeleteConfirmation };
פתרון תרגיל 3 - רכיב טאבים נגיש¶
'use client';
import { useState, useRef, useCallback, type KeyboardEvent, type ReactNode } from 'react';
interface TabItem {
id: string;
label: string;
content: ReactNode;
}
function AccessibleTabs({ tabs }: { tabs: TabItem[] }) {
const [activeTab, setActiveTab] = useState(0);
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
let newIndex = activeTab;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
newIndex = (activeTab + 1) % tabs.length;
break;
case 'ArrowLeft':
e.preventDefault();
newIndex = (activeTab - 1 + tabs.length) % tabs.length;
break;
case 'Home':
e.preventDefault();
newIndex = 0;
break;
case 'End':
e.preventDefault();
newIndex = tabs.length - 1;
break;
default:
return;
}
setActiveTab(newIndex);
tabRefs.current[newIndex]?.focus();
},
[activeTab, tabs.length]
);
return (
<div>
{/* רשימת טאבים */}
<div role="tablist" aria-label="טאבים" onKeyDown={handleKeyDown}>
{tabs.map((tab, index) => (
<button
key={tab.id}
ref={(el) => { tabRefs.current[index] = el; }}
role="tab"
id={`tab-${tab.id}`}
aria-selected={activeTab === index}
aria-controls={`panel-${tab.id}`}
tabIndex={activeTab === index ? 0 : -1}
onClick={() => setActiveTab(index)}
style={{
padding: '8px 16px',
border: 'none',
borderBottom: activeTab === index ? '2px solid blue' : '2px solid transparent',
backgroundColor: 'transparent',
cursor: 'pointer',
fontWeight: activeTab === index ? 'bold' : 'normal',
}}
>
{tab.label}
</button>
))}
</div>
{/* פאנלים */}
{tabs.map((tab, index) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeTab !== index}
tabIndex={0}
style={{ padding: '16px' }}
>
{tab.content}
</div>
))}
</div>
);
}
// דוגמת שימוש
function TabsExample() {
const tabs: TabItem[] = [
{ id: 'general', label: 'כללי', content: <p>הגדרות כלליות</p> },
{ id: 'security', label: 'אבטחה', content: <p>הגדרות אבטחה</p> },
{ id: 'notifications', label: 'התראות', content: <p>הגדרות התראות</p> },
];
return <AccessibleTabs tabs={tabs} />;
}
export { AccessibleTabs, TabsExample };
פתרון תרגיל 4 - Skip Link ו-Landmark Navigation¶
// components/SkipLink.tsx
function SkipLink() {
return (
<a
href="#main-content"
style={{
position: 'absolute',
top: '-40px',
left: 0,
background: '#000',
color: '#fff',
padding: '8px 16px',
zIndex: 100,
transition: 'top 0.2s',
}}
onFocus={(e) => {
(e.target as HTMLElement).style.top = '0';
}}
onBlur={(e) => {
(e.target as HTMLElement).style.top = '-40px';
}}
>
דלג לתוכן הראשי
</a>
);
}
// components/BackToTop.tsx
'use client';
import { useCallback } from 'react';
function BackToTop() {
const scrollToTop = useCallback(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
// העברת פוקוס לתחילת הדף
const skipLink = document.querySelector<HTMLAnchorElement>('a[href="#main-content"]');
skipLink?.focus();
}, []);
return (
<button onClick={scrollToTop} aria-label="חזרה לראש הדף">
חזרה למעלה
</button>
);
}
// components/Layout.tsx
'use client';
import { usePathname } from 'next/navigation';
import { useEffect, useRef } from 'react';
function AccessibleLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const mainRef = useRef<HTMLElement>(null);
// העברת פוקוס ל-main בניווט בין דפים
useEffect(() => {
mainRef.current?.focus();
}, [pathname]);
return (
<>
<SkipLink />
<header role="banner">
<h1>האתר שלי</h1>
<nav aria-label="ניווט ראשי">
<ul>
<li><a href="/">דף הבית</a></li>
<li><a href="/products">מוצרים</a></li>
<li><a href="/about">אודות</a></li>
<li><a href="/contact">יצירת קשר</a></li>
</ul>
</nav>
</header>
<div style={{ display: 'flex' }}>
<main id="main-content" ref={mainRef} tabIndex={-1}>
{children}
</main>
<aside aria-label="תוכן צדדי">
<nav aria-label="ניווט משני">
<h2>קטגוריות</h2>
<ul>
<li><a href="/cat1">קטגוריה 1</a></li>
<li><a href="/cat2">קטגוריה 2</a></li>
</ul>
</nav>
</aside>
</div>
<footer role="contentinfo">
<p>כל הזכויות שמורות</p>
<BackToTop />
</footer>
</>
);
}
export default AccessibleLayout;
הסבר: ה-Layout כולל את כל אזורי ה-landmark הנדרשים. ה-Skip Link מופיע רק בפוקוס. שני אזורי הניווט מובדלים באמצעות aria-label. בניווט בין דפים, הפוקוס עובר אוטומטית ל-main.
פתרון תרגיל 5 - רשימת משימות נגישה¶
'use client';
import { useState, useRef, useCallback, type KeyboardEvent } from 'react';
interface Todo {
id: string;
text: string;
completed: boolean;
}
function AccessibleTodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodo, setNewTodo] = useState('');
const [announcement, setAnnouncement] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);
const addTodo = useCallback(() => {
if (!newTodo.trim()) return;
const todo: Todo = {
id: crypto.randomUUID(),
text: newTodo.trim(),
completed: false,
};
setTodos((prev) => [...prev, todo]);
setNewTodo('');
setAnnouncement(`משימה "${todo.text}" נוספה`);
inputRef.current?.focus();
}, [newTodo]);
const toggleTodo = useCallback((id: string) => {
setTodos((prev) =>
prev.map((todo) => {
if (todo.id === id) {
const updated = { ...todo, completed: !todo.completed };
setAnnouncement(
updated.completed
? `משימה "${todo.text}" סומנה כהושלמה`
: `משימה "${todo.text}" סומנה כלא הושלמה`
);
return updated;
}
return todo;
})
);
}, []);
const deleteTodo = useCallback(
(id: string, index: number) => {
const todo = todos.find((t) => t.id === id);
setTodos((prev) => prev.filter((t) => t.id !== id));
setAnnouncement(`משימה "${todo?.text}" נמחקה`);
// העברת פוקוס לפריט הבא, או הקודם, או לשדה הקלט
requestAnimationFrame(() => {
const items = listRef.current?.querySelectorAll<HTMLElement>('[role="listitem"]');
if (items && items.length > 0) {
const focusIndex = Math.min(index, items.length - 1);
items[focusIndex]?.focus();
} else {
inputRef.current?.focus();
}
});
},
[todos]
);
const handleItemKeyDown = useCallback(
(e: KeyboardEvent, id: string, index: number) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
deleteTodo(id, index);
} else if (e.key === ' ') {
e.preventDefault();
toggleTodo(id);
}
},
[deleteTodo, toggleTodo]
);
const completedCount = todos.filter((t) => t.completed).length;
return (
<div>
<h2>רשימת משימות</h2>
{/* הודעות לקורא מסך */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{announcement}
</div>
{/* הוספת משימה */}
<div role="search">
<label htmlFor="new-todo">הוסף משימה חדשה</label>
<input
ref={inputRef}
id="new-todo"
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') addTodo();
}}
aria-describedby="todo-instructions"
/>
<button onClick={addTodo}>הוסף</button>
<p id="todo-instructions" className="sr-only">
הקלד שם משימה ולחץ Enter או על כפתור הוסף
</p>
</div>
{/* מונה משימות */}
<p aria-live="polite">
{completedCount} מתוך {todos.length} משימות הושלמו
</p>
{/* רשימת המשימות */}
<ul ref={listRef} role="list" aria-label="רשימת משימות">
{todos.map((todo, index) => (
<li
key={todo.id}
role="listitem"
tabIndex={0}
onKeyDown={(e) => handleItemKeyDown(e, todo.id, index)}
aria-label={`${todo.text}${todo.completed ? ', הושלמה' : ''}`}
>
<input
type="checkbox"
id={`todo-${todo.id}`}
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
aria-checked={todo.completed}
/>
<label
htmlFor={`todo-${todo.id}`}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</label>
<button
onClick={() => deleteTodo(todo.id, index)}
aria-label={`מחק משימה: ${todo.text}`}
>
מחק
</button>
</li>
))}
</ul>
{todos.length === 0 && <p>אין משימות ברשימה</p>}
</div>
);
}
export default AccessibleTodoList;
פתרון תרגיל 6 - בדיקת נגישות¶
א. הגדרת eslint-plugin-jsx-a11y:
// .eslintrc.json
{
"plugins": ["jsx-a11y"],
"extends": ["plugin:jsx-a11y/strict"],
"rules": {
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/no-static-element-interactions": "error",
"jsx-a11y/alt-text": "error",
"jsx-a11y/label-has-associated-control": "error",
"jsx-a11y/no-autofocus": "warn",
"jsx-a11y/no-noninteractive-element-interactions": "error",
"jsx-a11y/role-has-required-aria-props": "error",
"jsx-a11y/interactive-supports-focus": "error"
}
}
ב. בדיקת axe בסביבת פיתוח:
// app/AxeProvider.tsx
'use client';
import { useEffect } from 'react';
function AxeProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
import('@axe-core/react').then((axe) => {
const React = require('react');
const ReactDOM = require('react-dom');
axe.default(React, ReactDOM, 1000, {
rules: [
{ id: 'color-contrast', enabled: true },
{ id: 'label', enabled: true },
],
});
});
}
}, []);
return <>{children}</>;
}
export default AxeProvider;
ג. בדיקת jest-axe:
// __tests__/accessibility.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('בדיקות נגישות', () => {
it('טופס הרשמה ללא בעיות נגישות', async () => {
const { container } = render(<AccessibleRegistrationForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('מודל ללא בעיות נגישות', async () => {
const { container } = render(
<AccessibleModal isOpen={true} onClose={() => {}} title="בדיקה">
<p>תוכן המודל</p>
<button>אישור</button>
</AccessibleModal>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('טאבים ללא בעיות נגישות', async () => {
const tabs = [
{ id: '1', label: 'טאב 1', content: <p>תוכן 1</p> },
{ id: '2', label: 'טאב 2', content: <p>תוכן 2</p> },
];
const { container } = render(<AccessibleTabs tabs={tabs} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('רשימת משימות ללא בעיות נגישות', async () => {
const { container } = render(<AccessibleTodoList />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
ד. רשימת בדיקה ידנית:
- ניווט בכל האתר באמצעות מקלדת בלבד (Tab, Shift+Tab, Enter, Space, Escape, חצים)
- בדיקה שיש אינדיקציית פוקוס ברורה וגלויה על כל אלמנט אינטראקטיבי
- בדיקה עם קורא מסך (VoiceOver) שכל התוכן נקרא בסדר הגיוני
- בדיקה שכל התמונות המשמעותיות כוללות טקסט חלופי מתאר
- בדיקה שהאתר ניתן לשימוש בזום 200% ללא אובדן תוכן או פונקציונליות
- בדיקת ניגודיות צבעים בכל הטקסטים והאלמנטים המשמעותיים (יחס 4.5:1 לפחות)
- בדיקה שהודעות שגיאה מוכרזות לקוראי מסך (aria-live או role="alert")
- בדיקה שטפסים כוללים labels מקושרים, שגיאות ברורות, והנחיות נגישות
- בדיקה שמודלים וחלונות קופצים כוללים מלכודת פוקוס וסגירה ב-Escape
- בדיקה שסרטונים ואודיו כוללים כתוביות או תמלול
תשובות לשאלות¶
1. ההבדל בין aria-label, aria-labelledby ו-aria-describedby:
aria-label- מספק תווית טקסטואלית ישירה לאלמנט. משמש כשאין תווית גלויה. דוגמה:<button aria-label="סגור">X</button>aria-labelledby- מצביע על אלמנט אחר שמשמש כתווית (באמצעות ה-id שלו). משמש כשהתווית כבר קיימת בדף. דוגמה:<div aria-labelledby="section-title">כאשר יש<h2 id="section-title">כותרת</h2>aria-describedby- מצביע על אלמנט שמספק תיאור נוסף (מעבר לתווית). משמש להנחיות, שגיאות או מידע משלים. דוגמה: שדה סיסמה עםaria-describedby="password-help"שמצביע על טקסט עם דרישות הסיסמה
2. למה לא tabIndex חיובי:
tabIndex חיובי (1, 2, 3...) שובר את סדר הטאב הטבעי של ה-DOM. אלמנטים עם tabIndex חיובי מקבלים פוקוס לפני כל שאר האלמנטים, מה שיוצר חוויה מבלבלת ולא צפויה. קשה מאוד לתחזק את הסדר כשמוסיפים או מסירים אלמנטים. השימוש ב-0 (סדר טבעי) ו-1- (רק תכנותי) מספיק לכל המקרים.
3. aria-live polite מול assertive:
polite- קורא המסך ימתין שיסיים את ההקראה הנוכחית לפני שיקריא את העדכון. מתאים לעדכונים שאינם דחופים. דוגמה: "הפריט נוסף לעגלת הקניות"assertive- קורא המסך יפסיק את ההקראה הנוכחית ויקריא את העדכון מיד. מתאים להודעות דחופות. דוגמה: "שגיאה: החיבור לשרת נותק"
4. הכלל הראשון של ARIA:
הכלל אומר: אם אפשר להשתמש באלמנט HTML סמנטי, אל תשתמש ב-ARIA. אלמנטים סמנטיים (button, nav, input) כבר כוללים תפקידים, מצבים והתנהגויות מובנות. שימוש ב-ARIA מוסיף מורכבות ויכול לגרום לטעויות. למשל, <button> כבר תומך בפוקוס, Enter, Space ומוכרז כ-"כפתור", בעוד ש-<div role="button"> דורש הוספה ידנית של כל אלה.
5. מלכודת פוקוס (Focus Trap):
מלכודת פוקוס מגבילה את ניווט ה-Tab כך שהפוקוס נשאר בתוך אלמנט מסוים ולא יוצא ממנו. כשמגיעים לאלמנט האחרון ולוחצים Tab, הפוקוס חוזר לראשון (ולהיפך עם Shift+Tab). היא חשובה כי בלעדיה, משתמשי מקלדת עלולים "ללכת לאיבוד" מאחורי מודל פתוח. משתמשים בה במודלים (dialogs), תפריטי dropdown, ומגירות צד (drawers) - כל מקרה שבו יש שכבת UI שמכסה את שאר הדף.