נגישות ב-Generative UI: הפיכת ממשקי AI לכוללניים
מדריך מעשי להפיכת ממשקים גנרטיביים לנגישים לכלל המשתמשים, כולל קוראי מסך וניווט במקלדת.
למה נגישות קשה יותר עם Generative UI
צוות הנגישות שלכם אישר כבר כל מסך במוצר. שלושה שבועות לאחר מכן, ה-AI ממציא פריסה שאף מעצב לא שרטט — ולפריסה הזו יש היררכיית כותרות שבורה לקוראי מסך, focus trap בדיאלוג שנוצר, ותרשים שהצבע הוא האות היחיד שלו. אף אחד מאלה לא נתפס בסקירה כי אף אחד מאלה לא קיים בזמן הסקירה.
זהו משטח הנגישות החדש, והמדריך הישן לא מכסה אותו.
בממשק מסורתי, מהנדס יכול לבדוק כל מסך ולאמת שהוא עומד בדרישות WCAG 2.2. מספר המסכים סופי. צוות הנגישות יודע בדיוק מה לבדוק.
Generative UI שובר את המודל הזה. קבוצת הממשקים האפשריים אינה ניתנת לספירה — ה-AI יכול להרכיב רכיבים בדרכים שאף אדם לא עיצב במפורש. מסך שעובר בדיקת נגישות היום עשוי להשתלב עם רכיב חדש שנוסף מחר וליצור פריסה לא נגישה.
הפתרון הוא לדחוף דרישות נגישות לרמת הרכיב. אם כל רכיב בספריה שלכם נגיש בנפרד, כל הרכבה שלהם תהיה נגישה — בתנאי שההרכבה עצמה מובנית נכון. הסייג הזה עושה עבודה כבדה; נחזור אליו בסעיף "נגישות קומבינטורית", כי שם חיים רוב באגי ה-a11y של ממשקים גנרטיביים.
מודל זה ממוקד-רכיב הוא נקי יותר מביקורת ידנית של כל מסך. הוא גם לא אופציונלי: ה-AI לא יוסיף תוויות ARIA או ינהל פוקוס בשבילכם. ספריית הרכיבים היא נקודת המינוף היחידה שלכם.
הבסיס ברמת הרכיב
כל רכיב ברג'יסטרי כלי ה-Generative UI שלכם חייב לעמוד בדרישות הבאות באופן עצמאי:
קודם כל HTML סמנטי. השתמשו ב-<button> לכפתורים, ב-<nav> לניווט, ב-<table> לנתונים טבלאיים. אל תשתמשו ב-<div onClick={...}> כשיש רכיב סמנטי מתאים.
// שגוי: div שמתחזה לכפתור
<div className="button" onClick={handleClick}>Submit</div>
// נכון: רכיב כפתור אמיתי
<button type="button" onClick={handleClick}>Submit</button>
לכל התמונות יש alt text. לתמונות דקורטיביות: alt="". לתמונות אינפורמטיביות, כתבו תיאור.
צבע אינו האות היחיד. תרשים שמציג ערכים חיוביים בירוק ושליליים באדום צריך אינדיקטור נוסף למשתמשים שאינם מבחינים בין אדום לירוק — סימן + / -, אייקון, או תווית טקסט.
function TrendIndicator({ value }: { value: number }) {
const isPositive = value >= 0;
return (
<span
className={isPositive ? 'text-green-600' : 'text-red-600'}
aria-label={isPositive ? `Up ${Math.abs(value)}%` : `Down ${Math.abs(value)}%`}
>
{/* Icon provides visual signal beyond color */}
{isPositive ? '↑' : '↓'} {Math.abs(value)}%
</span>
);
}
אלמנטים אינטראקטיביים נגישים במקלדת. כל כפתור, קישור ושדה טופס ברכיבים שלכם חייב להיות ניתן לפוקוס ולהפעלה במקלדת בלבד.
משטחי מגע גדולים מספיק. WCAG 2.5.5 (AA) דורש 44×44 פיקסלים CSS לרכיבים אינטראקטיביים. במובייל, מטרות מגע קטנות הן מקור ראשוני לכשלי נגישות.
אזורי ARIA Live לתוכן בסטרימינג
סטרימינג הוא המאפיין המגדיר של Generative UI — רכיבים מופיעים בהדרגה כש-AI מייצר אותם. קוראי מסך אינם מכריזים אוטומטית על תוכן שמופיע דינמית. עליכם לציין זאת במפורש.
השתמשו ב-aria-live כדי להכריז כשתוכן חדש שנוצר מגיע:
// components/genui-output-region.tsx
export function GenUIOutputRegion({ children, isLoading }: {
children: React.ReactNode;
isLoading: boolean;
}) {
return (
<div
aria-live="polite"
aria-busy={isLoading}
aria-label="AI-generated content"
aria-atomic="false"
>
{children}
</div>
);
}
בחירות מרכזיות כאן:
aria-live="polite"מכריז על תוכן חדש בזמן הפנוי הבא — בלי להפריע למשתמש באמצע משפט כמו שהיה עושהassertive.aria-busy={isLoading}מודיע לטכנולוגיה המסייעת שהאזור מתעדכן. קוראי מסך מחזיקים הכרזות עד ש-aria-busyהופך ל-false.aria-atomic="false"מכריז על הוספות בודדות כשמגיעות, במקום לקרוא מחדש את כל האזור בכל פעם.
למצב ה-skeleton של הטעינה:
function LoadingSkeleton({ label }: { label: string }) {
return (
<div
role="status"
aria-label={`Loading ${label}`}
className="animate-pulse rounded-lg bg-muted h-32"
/>
);
}
role="status" הוא אזור aria-live="polite" מרומז להודעות מצב קצרות. הוא מכריז כשמופיע מבלי להפריע לדיבור הנוכחי.
ניהול פוקוס
כשתוכן שנוצר מופיע, פוקוס המקלדת נשאר במקומו. בדרך כלל זה נכון — אתם לא רוצים שפוקוס ייקפץ כש-AI מסטרים רכיבים. אבל לחלק מהאינטראקציות צריך להזיז את הפוקוס מפורשות.
אחרי הגשת טופס שמחליפה את תוכן הדף:
const outputRef = useRef<HTMLDivElement>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const ui = await generateUI(prompt);
setGeneratedUI(ui);
// Move focus to the output area after generation completes
// Small timeout ensures the DOM has updated
setTimeout(() => {
outputRef.current?.focus();
}, 50);
}
// Add tabIndex to make the div focusable
<div ref={outputRef} tabIndex={-1} aria-label="Generated results">
{generatedUI}
</div>
tabIndex={-1} הופך את האלמנט לניתן לפוקוס תכנותי מבלי להוסיף אותו לסדר ה-Tab. המשתמש יכול לעבור עליו בטבעיות, אבל אפשר לפקס אותו עם .focus().
אחרי שנפתח דיאלוג או פאנל עם תוכן שנוצר:
הזיזו פוקוס לאלמנט הניתן לפוקוס הראשון בתוך הפאנל, או לכותרת הפאנל. החזירו פוקוס לאלמנט הטריגר כשהפאנל נסגר.
ניווט במקלדת ברכיבים שנוצרו
רכיבים שמופיעים בפריסות שנוצרו חייבים להיות ניתנים לניווט מלא במקלדת. בדקו כל רכיב:
טבלאות: ניווט עם מקשי חצים בין תאי טבלה הוא ציפייה של משתמשי קוראי מסך. אם רכיב DataTable שלכם לא ממש זאת, הוא מהווה מחסום מקלדת לטבלאות מורכבות.
תרשימים: ספקו חלופה טבלאית. תרשימי SVG עשירים ויזואלית אך כמעט חסרי משמעות לקוראי מסך. הוסיפו אלמנט <details> או טבלה נסתרת ויזואלית עם נתוני התרשים.
function BarChart({ title, data }: BarChartProps) {
return (
<div>
<h3>{title}</h3>
{/* Visual chart */}
<svg aria-hidden="true">
{/* ... chart rendering ... */}
</svg>
{/* Accessible data table, visually hidden */}
<details className="sr-only">
<summary>View data as table</summary>
<table>
<caption>{title}</caption>
<thead>
<tr><th>Category</th><th>Value</th></tr>
</thead>
<tbody>
{data.map(({ label, value }) => (
<tr key={label}>
<td>{label}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</details>
</div>
);
}
המחלקה sr-only מסתירה את הטבלה ויזואלית תוך שמירתה בעץ הנגישות. aria-hidden="true" על ה-SVG מונע מקוראי מסך לנסות לפרש את ה-markup הגולמי של SVG.
תנועה מופחתת
חלק מהמשתמשים מגדירים את מערכת ההפעלה שלהם לכבד העדפה לתנועה מופחתת — כי אנימציות גורמות לאי-נוח פיזי לאנשים עם הפרעות וסטיבולריות. skeleton של טעינה ואנימציות מעבר חייבים לכבד העדפה זו.
/* In your global CSS or Tailwind config */
@media (prefers-reduced-motion: reduce) {
.animate-pulse {
animation: none;
}
.transition-all {
transition: none;
}
}
ב-Tailwind, אפשר להשתמש בווריאנטים motion-safe: ו-motion-reduce::
<div className="motion-safe:animate-pulse motion-reduce:opacity-50 bg-muted rounded-lg h-32" />
motion-safe: מיושם רק כשהמשתמש לא ביקש תנועה מופחתת. motion-reduce: מיושם כשביקש. למצבי טעינה, placeholder סטטי מעט מואפל הוא חלופה טובה לתנועה מופחתת במקום האנימציה הפועמת.
היררכיית כותרות בפריסות מורכבות
ה-AI מרכיב רכיבים לפריסות. לכל רכיב עשויה להיות כותרת משלו. כשכמה רכיבים מופיעים יחד, כותרותיהם חייבות לצור היררכיה קוהרנטית — לא ציד של H2-ים מנותקים.
זוהי בעיית הרכבה שלא ניתן לפתור ברמת הרכיב הבודד. כל רכיב צריך לקבל prop של רמת כותרת:
interface MetricCardProps {
label: string;
value: string;
change: number;
headingLevel?: 'h2' | 'h3' | 'h4'; // default to h3
}
function MetricCard({ label, value, change, headingLevel: Heading = 'h3' }: MetricCardProps) {
return (
<div className="rounded-lg border p-6">
<Heading className="text-sm font-medium text-muted-foreground">{label}</Heading>
{/* ... */}
</div>
);
}
בהגדרת הכלי שלכם, כללו את רמת הכותרת כפרמטר שה-AI יכול לקבוע:
metricCard: {
description: 'Display a KPI metric. Use headingLevel h2 for the first metric in a section, h3 for subsequent metrics.',
parameters: z.object({
label: z.string(),
value: z.string(),
change: z.number(),
headingLevel: z.enum(['h2', 'h3', 'h4']).default('h3'),
}),
}
כלי בדיקה
השתמשו בכלים הבאים לביקורת ספריית הרכיבים ופלטים שנוצרו:
axe-core: בדיקת נגישות אוטומטית שתופסת ~30% מבעיות הנגישות אוטומטית. שלבו עם jest-axe לכיסוי unit test.
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('MetricCard has no accessibility violations', async () => {
const { container } = render(
<MetricCard label="Revenue" value="$84,200" change={12.4} />
);
expect(await axe(container)).toHaveNoViolations();
});
תוסף נגישות Storybook: הריצו בדיקות axe ישירות ב-Storybook בזמן הפיתוח. תופס בעיות לפני שמגיעות לבדיקות.
בדיקת קוראי מסך: NVDA (Windows, חינמי) ו-VoiceOver (macOS, מובנה) חיוניים לבדיקת החוויה שכלים אוטומטיים לא יכולים למדוד — עד כמה התוכן שנוצר מובן כשנקרא בקול?
ניווט במקלדת בלבד: נתקו את העכבר ונווטו באפליקציה שלכם באמצעות Tab, Shift+Tab, Enter, Space ומקשי חצים בלבד. זו הדרך הכי מהירה לאתר מלכודות מקלדת.
סיכום הדרישות שלא ניתן לוותר עליהן
לפני שמשחררים פיצ'ר Generative UI:
- כל רכיב ברג'יסטרי הכלים עובר axe ללא הפרות
- כל האלמנטים האינטראקטיביים נגישים ופועלים במקלדת
- צבע לעולם אינו האות היחיד למשמעות
- אזור
aria-liveעוטף את הפלט המסטרים - ל-skeletons יש
role="status"ו-aria-labelתיאורי - לתרשימי SVG יש חלופת טבלת נתונים
- כל האנימציות מכבדות את
prefers-reduced-motion - רמות כותרת הן פרמטר בקונפיגורציה של הרכיבים, לא hard-coded
נגישות שמובנית בספריית הרכיבים אינה נטל — היא מה שהופך את ההבטחה של "AI יכול להרכיב הכל" לאמיתית עבור כלל המשתמשים.
בונים Generative UI נגיש לאפליקציה מורכבת? בואו נעבור יחד על הפרטים הספציפיים.
Alex
מהנדס וייעוץ Generative UI
מהנדס בכיר המתמחה בממשקי AI ומערכות Generative UI. מסייע לצוותי מוצר לשלוח מהר יותר עם ה-stack הנכון.
מאמרים קשורים
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
הישארו קדימה ב-Generative UI
מאמרים שבועיים, עדכוני framework ומדריכי יישום מעשיים — ישירות לתיבת הדואר.
זקוקים לעזרה ביישום מה שקראתם?
קבעו ייעוץ חינם