Taarik Ashenafi
Taarik Ashenafi is a former software engineering intern on the accessibility team at GitHub.
We built a plugin for the GitHub Accessibility Scanner to make sure your alt text is actually accessible. Here’s how it works.
More than one in four images on the web’s most popular home pages have alt text that’s missing, vague, or copied from adjacent images.
That’s from WebAIM’s 2026 WebAIM Million report, which found that alt text,an HTML attribute containing text describing the content of an image, was missing on 16.2% of images across the top million home pages. Among the images that did have alt text, another 10.8% provided an undescriptive attribute, such as alt="image"
, a raw filename, or a description duplicated from a neighbor.
While automated tooling reliably flags missing alt text, it isn’t as good at fixing poorly written alt text. Most alt text checkers test whether an accessible name for an image exists, not whether the provided alt text says anything useful about the associated image, and that’s a deliberate design choice: a quality-oriented rule with false positives is a rule teams switch off. So alt="IMG_2847.png"
passes. So does the same alt="3/5 stars"
on five different star-shaped icons.
We built an alt text plugin for the GitHub Accessibility Scanner to help improve your alt text. This post covers where we drew the line between what a checker can prove and what it can only suspect, why our worst bug turned out to be a layout problem rather than a parsing one, and what changed once we let a model into the loop.
If you’re building automated checks of your own, for accessibility or otherwise, the tradeoffs should transfer.
Presence of alt text is an objective fact; the attribute is there or it isn’t. Quality is often a judgment call. A machine can’t prove whether a sentence adequately describes a picture in context from markup.
However, not all quality is subjective. There’s several checks you can perform based on the alt text alone, with no need to consult the image content:
hero.png
, IMG_2847.jpg
.TODO
, tbd
.image
, logo
, chart
.Every one of those is a claim about a string, and that became our dividing line. Five deterministic rules run by default which need no credentials for running AI models or network calls. One opt-in rule calls a model with provided image content and surrounding context, for judgments an alt text string can’t support on its own.
First, we had to determine which images to judge on a scanned webpage. We use Playwright’s role-based locator rather than querySelectorAll('img')
, so anything not included in the browser’s accessibility tree drops out, including anything carrying alt=""
. That last exclusion matters most. An empty alt is the author explicitly saying the image is decorative, and flagging it would punish exactly the behavior you want to encourage.
So, how strict should it be? A quality checker lives or dies on false positives, so we chose closed sets over clever heuristics. The vague-alt rule normalizes a string, then checks it against a curated list of words that carry no information on their own. It fires only on an exact match:
alt="image"
gets flagged.alt="image of the login screen with the SSO button highlighted"
doesn’t.Rules this literal miss plenty of bad alt text. We took the miss over the false positive, because a reliable checker that developers enable beats one that gets switched off.
Repeated alt text presented an interesting problem. Picture a row of five star-shaped icons that each say "3/5 stars"
. A screen reader user hears the same thing five times and learns nothing new from four of them.
Our first version walked the images in document order and flagged any run sharing the same normalized alt. It caught things it shouldn’t have. For example, a footer “GitHub” logo and a header “GitHub” logo might sit next to each other in the extracted list but nowhere near each other on screen, so nobody experiences them as a group.
What matters is where images land on screen, not where they sit in the markup. So the rule now checks page layout, and only extends a run when the gap between two bounding boxes is small compared to the boxes themselves:
const gap = Math.max(horizontalGap, verticalGap)
const largerDim = Math.max(a.boundingBox.width, a.boundingBox.height,
b.boundingBox.width, b.boundingBox.height)
return gap > GAP_MULTIPLIER * largerDim
Two details worth noting:
Deterministic rules only need the alt string. Anything smarter needs to know what the page is about, and none of that is tracked by the image element. Whether alt="a smiling person"
is fine depends entirely on what surrounds it: on a generic mood shot, it’s probably works. But under a heading where a specific person is named, it doesn’t provide enough detail.
In our optional alt-text-quality
check, we extract page context alongside each image: the nearest heading, the page title, any
, whether the image sits inside a link or button, and up to 600 characters of nearby prose.
The link signal matters most, because when an image is a link’s only content, its alt becomes the link’s accessible name. The right alt then names the destination instead of describing the picture.
One caution: The plugin only records that an image sits inside a link. We don’t check whether it’s the link’s only content, which is the part that actually turns alt into a link name. So right now both cases look identical to the model.
That context, the alt, and the image go to a vision model through GitHub Models. Our failure modes were rarely the model misreading a picture. They were the model having opinions. Given perfectly good alt text, our first version of the checker would suggest different alt text, because “could this be better?” is a question a language model always answers yes to. Every image becomes a finding, so the signal disappears.
Three changes fixed it:
reasoning
is generated before verdict
and the model has to build an argument before it picks a label.None of that makes the model unfailingly correct. It makes it consistent enough to iterate against. The repository carries an offline grading harness built from published teaching material: WebAIM, the W3C images tutorial, and POET. The rule and the harness share one prompt, so what you tune offline is what runs in CI. That harness only tests the model’s judgment, though, not the whole pipeline. A case can score perfectly there and never reach the model in a real scan.
The moment a check calls an external model with webpage data, it stops being just a lint rule and requires careful data flow design. A few things follow from that:
href
s often carry signed CDN tokens or session identifiers, so query and fragment are stripped from anything entering the model context or the rule’s error logs. For the same reason, src
and srcset
are replaced with (omitted)
in the markup we send.One caution, because that list is easy to over-read: findings still carry the real page URL and original HTML into the scanner’s normal reporting pipeline. That’s on purpose, since you can’t fix an image you can’t locate. Redaction narrows what reaches the model and the logs, not what lands in your own issues. And if you set up Azure AI Vision credentials, an optional OCR pre-pass sends image bytes to a second place. Nothing requires Azure, but a data-flow review needs to cover both paths.
Cost follows the same shape. In the common case this is one model call per image per scan, which on an image-heavy site dominates the cost of the whole run. That’s reason enough to put it on a schedule rather than on every commit.
alt
attribute rather than the computed accessible name, so an aria-label
that fixes the problem won’t stop the finding.missing-alt
rule covers the same ground.
tags. SVG, role="img"
containers, CSS backgrounds, and canvas aren’t covered yet.Separate what you can prove from what you can only suspect, and give them different defaults. Checks that prove something should be cheap, predictable, and on by default. Checks that only suspect something should be opt-in, and should read as a suggestion rather than a verdict. Then, ask what the user experiences rather than what the DOM says. Every gap still open in this plugin has that second shape. We record that an image is inside a link, not that it is the link. We read an attribute, not a computed name.
That distance is the real boundary, and a better model doesn’t close it. Deciding what the functionality of an image is for a user who can’t see it still requires human judgment. What automation buys you is making sure that human is giving the right images a second examination.
Try the alt-text plugin in your accessibility scanning workflow. If it tells you the wrong thing, please report it. Open an issue with the finding and, if public, a link to the affected page.
Enterprise Java developers have a new superpower—drive GitHub Copilot from idiomatic Java code with annotations, virtual threads, and more.
Instead of one huge, un-reviewable pull request, teach coding agents to decompose work into a clean, ordered stack with GitHub stacked pull requests.
How a branch-free loop and byte-space arithmetic let GitHub case-fold every byte of code search at >45 GiB/s on a single core.
Facts Only
* Taarik Ashenafi is a former software engineering intern on the accessibility team at GitHub.
* A plugin was developed for the GitHub Accessibility Scanner to evaluate image alt text.
* The WebAIM 2026 WebAIM Million report found alt text was missing on 16.2% of images on the top million home pages.
* The same report found 10.8% of images with alt text provided undescriptive attributes.
* The plugin utilizes five deterministic rules and one opt-in AI model rule.
* Playwright’s role-based locator is used to identify images in the browser’s accessibility tree.
* Deterministic rules flag exact matches against a curated list of vague words or detect repeated strings.
* The repeated alt text rule uses bounding box calculations to determine if images are visually adjacent.
* The optional AI check utilizes GitHub Models, analyzing the image, alt text, and surrounding page context.
* Page context includes the nearest heading, page title, 600 characters of nearby prose, and link/button status.
* Personal identifiers in URLs (query and fragment) and image sources are stripped before being sent to the model.
Executive Summary
The GitHub Accessibility Scanner has been expanded with a plugin designed to move beyond simply detecting the presence of alt text to evaluating its quality. This effort addresses a widespread issue where a significant percentage of web images either lack descriptions or use non-descriptive placeholders that provide no value to screen reader users. To avoid the high false-positive rates that lead developers to disable automated tools, the system distinguishes between objective failures—such as filename-based alt text—and subjective quality assessments.
The tool employs a tiered approach: deterministic rules run by default to catch blatant errors, while a vision-based AI model is available as an opt-in feature for nuanced judgment. This model incorporates broader page context, such as nearby headings and prose, to determine if a description is sufficient for the specific environment. While the AI introduces potential costs and data privacy considerations, the implementation includes redaction of sensitive URL tokens. Ultimately, the system is framed as a support mechanism to highlight images for human review rather than a replacement for human judgment in accessibility design.
Full Take
This technical walkthrough operates in Constructive Mode, functioning as a practical guide for engineers balancing automation with human-centric design. The strongest version of this narrative is a pragmatic admission of the limits of AI: it acknowledges that "quality" in accessibility is a judgment call that cannot be fully solved by markup analysis alone. By separating "proven" errors from "suspected" errors, the approach respects the developer's workflow and the end-user's actual experience.
The underlying paradigm is one of "augmented auditing." It assumes that the primary bottleneck in accessibility is not a lack of tools, but the "noise" of false positives that causes developers to ignore them. The decision to prioritize a "miss" over a "false positive" is a sophisticated nod to the psychology of tooling adoption. However, a generative question remains: as vision models become more consistent, will the industry shift toward "AI-suggested" alt text by default, potentially eroding the human intentionality required to decide what an image actually *means* in a specific context?
The implementation highlights a critical tension in modern software: the trade-off between deep analysis and data privacy/cost. The need to redact CDN tokens and schedule scans rather than running them on every commit reveals the "hidden tax" of integrating LLMs into CI/CD pipelines.
Bridge Questions:
1. If AI begins generating the alt text it is also tasked with auditing, how do we prevent a feedback loop of "plausible but inaccurate" descriptions?
2. How does the reliance on the browser's accessibility tree impact images delivered via CSS or Canvas, and does this create a blind spot in the audit?
Counterstrike Scan: The content is a transparent engineering post; it does not match the patterns of a coordinated influence campaign.
