AI-Assisted Accessibility Passes That Actually Ship
Accessibility work stalls because it's one undifferentiated backlog card. AI is good at turning it into forty reviewable diffs — provided you run the deterministic scanners first and keep every judgement call, because it can't see pixels, focus order, or your users.
Hessam Rastegari
Senior Android Developer · 12 years shipping Android
TL;DR — AI doesn't fix accessibility; it converts an unshippable backlog card into forty small diffs,
which is the actual blocker. Run the deterministic tools first (Accessibility Scanner finds contrast and
touch-target failures in rendered pixels, which no model can), then use the model for semantics gaps,
heading structure, regression tests, and one high-value trick: "write what a screen reader user hears, in
order." Keep every judgement call — it adds contentDescription to decorative icons (making TalkBack
worse), can't tell a useful description from a passing one, can't verify contrast, can't see focus order,
and defaults to verbose when real users want terse.
Why accessibility work doesn't ship
It isn't disagreement. Nobody argues against it in a planning meeting. It doesn't ship because of how the work is shaped: one ticket called "accessibility pass", unestimable, unassignable, with no definition of done. It gets pulled into a sprint, someone spends two days, the ticket is still open, and it rolls.
Everything below is aimed at that shape, not at your team's intentions. The model's real contribution is decomposition — turning a mountain into a list of diffs each small enough to review in five minutes.
Step 1: run the free tools first, and don't skip this
A model is the wrong instrument for anything a deterministic tool already catches, and it's worse at it.
- Accessibility Scanner (Play Store, from Google) on the running app. It sees rendered pixels: touch targets under 48dp, text contrast below 4.5:1, missing labels, duplicated descriptions. This is the single highest-value tool in this post and it costs nothing.
- Lint —
ContentDescription,ClickableViewAccessibilityand friends. Modifier.semantics { testTagsAsResourceId = true }so UI Automator and scanning tools can see your Compose nodes at all.- Espresso's
AccessibilityChecks.enable()in your instrumentation tests, which turns a subset of these into CI failures.
Feed the output of these into the model rather than asking it to duplicate them. "Here are 34 scanner findings and the source of the four screens they're on — group them, and propose a diff per screen" is a good use of a model. "Find accessibility problems in this file" is a worse version of a linter.
Step 2: the prompt that's worth more than the rest
Not a fix request. A simulation request:
Here is a Compose screen. Write, in order, exactly what a TalkBack user
hears as they swipe through it — one line per focusable node, including
the role and state announcements. Mark any node whose announcement would
be empty or duplicated.
Then read the output aloud.
1. "Order details" (no heading role)
2. "Button" ← empty
3. "Button" ← empty
4. "Image" ← decorative? unclear
5. "£42.10" (no context — total? item?)
6. "Button. Double tap to activate." ← which button?
Ten seconds of reading that does more than an audit document, because it makes the experience concrete rather than describing it. It also produces exactly the artefact a PM or designer can react to, which is usually what unblocks the work politically.
Every line marked empty or ambiguous becomes one small ticket. That's the decomposition.
Step 3: what it's genuinely good at
Finding missing semantics in Compose. Modifier.clickable without an onClickLabel, a custom switch
built from a Box with no Role.Switch, a row of a label and value that should be one merged node instead
of two focus stops:
Row(
modifier = Modifier
.clickable(onClickLabel = "Open order details") { open() }
.semantics(mergeDescendants = true) {}
) {
Text("Order #4021")
Text("Delivered")
}
Heading structure. Ask for the hierarchy a screen reader would expose and you'll usually find there isn't one. Headings are how a screen reader user skips — without them, a long screen can only be traversed linearly, one node at a time.
Text(
text = "Payment",
modifier = Modifier.semantics { heading() },
)
State descriptions, which teams forget entirely — a toggle that announces "on" but not what it is, a loading state that announces nothing at all.
The regression test, so the work survives the next redesign:
@Test fun avatarHasMeaningfulLabel() {
composeRule.onNodeWithContentDescription("Profile photo of Hessam")
.assertExists()
}
Step 4: where it is confidently wrong
This is the section to keep, because each of these produces a diff that looks correct in review.
It adds contentDescription to decorative icons. A chevron next to text saying "Settings" should be
contentDescription = null — it adds a focus stop and says nothing. Models strongly prefer filling a
nullable field, so this error arrives in volume, and it makes the screen reader experience actively worse
than before your "fix". Rule: if the neighbouring text already says it, the icon is null.
It can't tell a useful description from a passing one. "Image" satisfies every automated check. "Photo of your parcel left in the porch" is the one that has value. Which of those a user needs depends on what your app is for — and the model has no access to that.
It cannot verify contrast. It will compute a ratio from your theme's hex values and be arithmetically correct, while the actual text renders over a photograph or a gradient. Contrast is a property of pixels. That's why the scanner comes first.
It can't see focus order, which is where the genuinely broken experiences live — a dialog that doesn't trap focus, a bottom sheet that leaves focus behind it, a screen where TalkBack jumps from the header to the footer and back. All runtime behaviour, all invisible in source.
It defaults to verbose. Model-written descriptions read like alt text written to be graded. Experienced screen reader users navigate fast and find long announcements exhausting. Cut every one you accept — usually by half.
Step 5: the part nobody can do for you
Turn on TalkBack. Close your eyes. Complete your app's main flow.
Five minutes of this reorders your entire backlog, and it's the only step that tells you whether the app is usable rather than compliant. Everything above is preparation for it.
And if you can, get real feedback from someone who uses a screen reader daily. One session is worth more than every automated finding combined — they'll use the app in ways that don't occur to you, and they'll tell you which of your careful descriptions is noise.
The rule
AI makes the backlog shippable; it doesn't make the judgement calls — and accessibility is mostly judgement calls. Use it to decompose, to draft, and to write the test that stops the regression. Then run the scanner, put on TalkBack, and decide for yourself what your users actually need to hear.