Accessibility (a11y) for AI Products
AI accessibility has two layers: the product must be accessible, and AI can empower accessibility. This article covers AI product a11y essentials—semantic HTML/keyboard/ARIA/dynamic content, a11y control for AI-generated UI, AI empowerment (image descriptions/speech/text simplification), and an automated + manual a11y testing workflow.
Key Points
- Topic Focus: AI、React
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Accessibility (a11y) for AI products has an often-overlooked duality: your product must be accessible to users with disabilities, and your AI can in turn help users with disabilities overcome barriers. More critically—the core user base of AI products includes a significantly higher proportion of people who depend on assistive technology (screen readers, switch devices, voice control) than ordinary web apps. Visually impaired users use AI to read images, users with dyslexia use AI to simplify long texts, and hearing-impaired users rely on AI for real-time transcription. These people are the direct beneficiaries—or victims—of whether a11y is done well. Treating a11y as a checkbox exercise amounts to shutting out your most frequent, most loyal users.
1. The Dual Meaning of a11y in AI Apps
1. The product itself is accessible — your chat box, streaming output, generated UI must work with screen readers
2. AI empowers accessibility — use the model to fill users' sensory/cognitive gaps: alt text, transcription, text simplification
These two layers are often conflated, but in engineering terms they're completely different things. The former is frontend engineering (semantics, keyboard, ARIA, contrast, focus); the latter is product capability (treating the model as the user's "sensory proxy").
Also recognize this fact: AI is fundamentally a "threshold-lowering" tool, and visually impaired, dyslexic, elderly, and non-native speakers are more willing to use AI to accomplish things that were previously hard. So in AI products, a11y isn't an "edge requirement"—it's a hard metric on the core user path.
2. Basic a11y (React Perspective)
This part is the same as ordinary web, but there are several high-frequency pitfalls in React/Next.js.
1. Semantic HTML first; ARIA is a patch, not a replacement. The classic mistake: using <div onClick> as a button—Tab can't reach it, Enter doesn't trigger it, and screen readers read it as "text."
// Bad: <div onClick={handleSubmit}>Submit</div>
// Good:
<button onClick={handleSubmit}>Submit</button>
One principle: if a native semantic tag works, don't reach for ARIA. <button>, <nav>, <main>, <dialog>, <a> come with built-in semantics and keyboard behavior, while a <div> with role="button" is always an inferior choice—you have to hand-write tabIndex, onKeyDown, focus rings; miss one and it's a bug.
2. Keyboard reachable + visible focus. All interactive elements must be Tab-reachable and operable. Don't strip the focus ring with outline: none without replacing the style—this is the cheapest deduction in a11y. Custom interactive components must manage tabIndex and onKeyDown:
function Chip({ onSelect, label }: { onSelect: () => void; label: string }) {
return (
<span
role="button"
tabIndex={0}
onClick={onSelect}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelect();
}
}}
className="focus-visible:outline-2 focus-visible:outline-blue-500"
>
{label}
</span>
);
}
Use focus-visible instead of focus—the focus ring doesn't show on mouse clicks, only on keyboard operation, making the experience cleaner.
3. Focus management. On route changes, Modal open/close, or dynamically inserted content, don't lose focus. When a Modal opens, focus should move into the dialog; on close, return to the triggering button—this is the basics of "focus traps":
function Modal({ open, onClose, title, children }: Props) {
const panelRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (open) panelRef.current?.focus();
else triggerRef.current?.focus(); // On close, return focus to the triggering button
}, [open]);
if (!open) return null;
return (
<div role="dialog" aria-modal="true" aria-labelledby="modal-title"
tabIndex={-1} ref={panelRef} className="outline-none">
<h2 id="modal-title">{title}</h2>
{children}
</div>
);
}
tabIndex={-1} lets the container be programmatically focusable without entering the Tab sequence—this is the standard technique for Modal focus management.
4. Contrast + text scaling. WCAG AA requires 4.5:1 for body text and 3:1 for large text. Where AI products most love to trip on this: gray helper text (token counts, timestamps, "Copy" button tooltips). Use rem for font sizes, not px, so browser zoom and user preferences take effect.
5. Don't overuse ARIA. aria-label isn't a panacea—adding aria-label to a button that already has visible text overrides the original text and disrupts the mental model of voice users. Principle: visible text first; only reach for ARIA when semantics are insufficient.
3. a11y Unique to AI Apps: Dynamic Content
In ordinary pages, the DOM is static and stable; the screen reader reads it once and stops. AI apps are different—content dynamically grows—which introduces two unique challenges.
1. Is streaming output friendly to screen readers? No. If you stuff streaming tokens directly into an aria-live region, the screen reader reads every increment it receives—the user hears fragmented "Hel" "lo" "I" "am," completely unusable. This is the most typical a11y failure in AI apps.
Correct approach: don't let streaming text into the live region. Render streaming content normally for sighted users, and announce key states with a separate, visually hidden live region:
function AssistantMessage({
content,
status,
}: {
content: string;
status: "streaming" | "done" | "error";
}) {
return (
<>
{/* Streaming text renders normally, not a live region */}
<div className="prose">{content}</div>
{/* Visually hidden live region, only announces "status changes" */}
<div aria-live="polite" className="sr-only" aria-busy={status === "streaming"}>
{status === "streaming" && "AI is responding"}
{status === "done" && "Response complete"}
{status === "error" && "Response error"}
</div>
</>
);
}
Sighted users see streaming typing, voice users hear "AI is responding" once, and afterward use arrow keys to jump to the full message and read it themselves. Both sides are smooth.
2. Accessibility control for AI-generated UI. Many AI products let the model directly generate UI (tables, forms, cards). The model doesn't care about semantic HTML—it'll hand you <div> nested in <div>, hardcoded onclick, and <input> without labels. The only way to control this: the model outputs data; semantics are guaranteed by your components.
- Don't let the model spit out HTML to render directly. Have the model output structured data (JSON schema), and let the frontend render it with controlled, a11y-reviewed components:
// Model outputs { type: "table", columns: [...], rows: [...] }, not a <table> string
function GeneratedUI({ spec }: { spec: UISpec }) {
switch (spec.type) {
case "table":
return <DataTable columns={spec.columns} rows={spec.rows} />; // Component library already guarantees a11y
case "form":
return <SchemaForm fields={spec.fields} />; // Each field is auto-bound to a label
}
}
- If you must render model-generated markdown/HTML, run it through an allowlist sanitizer (allow semantic tags, strip
onclick/style/javascript:), then run jest-axe a11y assertions before releasing. In one sentence: neverdangerouslySetInnerHTML-render model output raw.
4. AI Empowering Accessibility
This is a unique advantage of AI products—the model in turn acts as the user's "sensory proxy," turning what traditional software can't do into product capabilities:
- Automatic alt text. When an image is uploaded, call a multimodal model to generate a description and fill in
alt. Note the distinction: decorative images getalt=""(empty, so the screen reader skips them), informational images get a description—don't blanket-fill or blank them all.
<img src={img.url} alt={img.informational ? (img.aiDescription ?? "") : ""} />
-
Speech transcription + subtitles. Transcribe audio/video content with Whisper-class models, generate WebVTT subtitles attached to
<track>, and do streaming transcription for real-time scenarios. This is the core entry point for hearing-impaired users, and a capability AI products should provide by default. -
Text simplification (for cognitive/reading disabilities). For long or technical texts, offer a "Simplified version" button that has the model rewrite it into shorter sentences and plainer language. Equally effective for low-literacy, elderly, and non-native users. Prompt constraint: "Use A2/B1 level vocabulary, short sentences, avoid jargon, preserve key information."
-
Real-time subtitles/sign language translation. In video calls and live streaming, streaming transcription overlaid with a signing avatar is the frontier of AI in a11y.
The criterion is direct: any sensory or cognitive gap the model can fill should be offered as a product capability—this is precisely the differentiation that sets AI products apart from traditional software, and the highest form of a11y.
5. a11y for Chat/Conversational Interfaces
Chat is the core interaction of AI products, and where a11y pitfalls are most concentrated. Four key points:
1. Correct tiering of aria-live.
- History message list: do not make it a live region. It's static, traversable content—make it an ordinary list (
<ol>/<li>orrole="log") so voice users can read it themselves with up/down arrow keys. - Status announcements (generating/done/error): use a separate
aria-live="polite"region, see section 3. - Urgent errors (e.g., content moderation interception):
aria-live="assertive", but use sparingly—it interrupts the user's current reading.
2. Buttons are reachable. Send, stop generating, copy, retry—all must be native <button>s, and icon buttons especially need aria-labels with sensible Tab order. The "Stop generating" button during streaming output must be instantly keyboard-triggerable—don't make the user wait.
<button aria-label="Stop generating" onClick={stop}>
<StopIcon />
</button>
3. Input fields need a visible label. Don't just use placeholder as the label—once the user types, the placeholder disappears and the screen reader can't read the field's meaning. Use <label htmlFor> or aria-label.
4. History messages are traversable. Use <ol>/<li> structure so voice users can browse message-by-message with reading keys, rather than it being treated as one giant blob of continuous text—the latter makes it impossible for users to locate "which reply was that."
6. a11y Testing: Automated + Manual
Relying only on manual or only on automated testing isn't enough—you need both layers stacked.
Automated layer (into CI, to catch basic mistakes):
pnpm add -D eslint-plugin-jsx-a11y @axe-core/playwright jest-axe
- eslint-plugin-jsx-a11y: catches missing
alt,onclickwithoutonkeydown,tabindexabuse, etc. at lint time. Zero cost, mandatory. - axe-core (jest-axe / @axe-core/playwright): runs against the real rendered DOM, asserts zero WCAG violations:
import { axe } from "jest-axe";
it("Chat page has no a11y violations", async () => {
const { container } = render(<Chat />);
expect(await axe(container)).toHaveNoViolations();
});
- Lighthouse a11y: gate the score in CI (e.g., ≥ 95), but remember Lighthouse only covers about thirty-odd items—a high score absolutely doesn't mean it's truly usable—it catches basic mistakes but can't catch interaction-level problems like "streaming output crashing the screen reader."
Manual layer (automated tests can't catch this, only humans can):
- Pure keyboard walkthrough: unplug the mouse, walk through the core flow with Tab/Enter/Esc/arrow keys—check whether focus is visible, whether you can exit the Modal, stop generation, copy. This single step eliminates more than half of real barriers, at very low cost.
- Real screen reader testing: on Windows use NVDA (free), on Mac use VoiceOver (built-in), on mobile TalkBack/VoiceOver. Actually run it: can you understand the streaming status? Can you traverse history messages? Can you hear error prompts?
- Reduced-motion preference: respect
prefers-reduced-motion; the streaming typewriter effect should be turned off or weakened under this preference—users with vestibular disorders can be triggered into dizziness by animation.
Automated catches 80% of basic mistakes; manual catches the remaining 20% of real experience issues. a11y without manual testing is essentially not done.
7. Treat a11y as a First-Class Citizen
a11y failures almost all stem from "patching afterward"—finding someone to go through it before launch, fixing contrast. This approach can only patch the surface; it can't patch into interaction flows, much less into semantic control of generated UI.
The right approach:
- At the design stage, draw focus order, streaming announcement strategy, and keyboard interactions into the prototype—don't wait until development is done to ask "can this be Tabbed."
- Build a11y into the component library. Choose a foundation like Radix UI / shadcn that's "unstyled, with built-in a11y" to eliminate
<div onClick>everywhere at the source. For components you build yourself, state in the DoD that they must pass axe assertions. - Put a11y into the DoD (Definition of Done). Every PR must pass eslint-plugin-jsx-a11y + axe assertions; core flows attach keyboard walkthrough notes.
- Treat it as a functional metric, not a "bonus." It ties directly to compliance risk (the EU EAA takes effect in 2025, US ADA lawsuits are frequent) and user growth—it's not charity, it's business.
Summary
a11y for AI products has two layers, and you can't skip either:
- Basic a11y: semantic HTML, keyboard reachability, focus management, contrast, ARIA restraint—the same hard skills as ordinary web.
- Dynamic content a11y: streaming text must not enter the live region—use a separate visually hidden region to announce status; model-generated UI goes through structured data + controlled components, never raw-rendering HTML.
- AI empowering accessibility: automatic alt text, transcription subtitles, text simplification—treating the model as the user's sensory proxy is a unique differentiator of AI products.
- Two-layer testing: automated (jsx-a11y + axe + Lighthouse) catches basic mistakes; manual (pure keyboard + NVDA/VoiceOver) catches real experiences—neither can be omitted.
- First-class citizen: built into the full chain of design, component library, and DoD—not a pre-launch patch.
Remember this: a11y isn't "goodwill toward a minority of users," it's fundamental engineering quality of AI products—especially when your core user base includes many who depend on assistive technology. Getting streaming output to be screen-reader-friendly and holding the line on generated UI semantics matters far more than adding one more flashy feature.
Related
Related Content
Article
What Is Vibe Coding? Will It Change How Programmers Work?
Technical trends, engineering practices, and an indie developer perspective on Vibe Coding, AI, and independent development.
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
Complete Technology Stack Analysis for LLM Application Development
Covers tech trends, engineering practices, and indie developer perspectives around large models, RAG, and AI.
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.