{"id":4914,"date":"2026-08-24T21:17:23","date_gmt":"2026-08-24T21:17:23","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/08\/24\/your-alt-text-passes-automated-checks-that-doesnt-mean-its-any-good\/"},"modified":"2026-08-24T21:17:23","modified_gmt":"2026-08-24T21:17:23","slug":"your-alt-text-passes-automated-checks-that-doesnt-mean-its-any-good","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/08\/24\/your-alt-text-passes-automated-checks-that-doesnt-mean-its-any-good\/","title":{"rendered":"Your alt text passes automated checks. That doesn\u2019t mean it\u2019s any good."},"content":{"rendered":"<p class=\"wp-block-paragraph\">More than one in four images on the web\u2019s most popular home pages have alt text that\u2019s missing, vague, or copied from adjacent images.<\/p>\n<p class=\"wp-block-paragraph\">That\u2019s from WebAIM\u2019s 2026 <a href=\"https:\/\/webaim.org\/projects\/million\/#alttext\">WebAIM Million<\/a> 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 <em>did<\/em> have alt text, another 10.8% provided an undescriptive attribute, such as <code>alt=\"image\"<\/code>, a raw filename, or a description duplicated from a neighbor.<\/p>\n<p class=\"wp-block-paragraph\">While automated tooling reliably flags missing alt text, it isn\u2019t 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\u2019s a deliberate design choice: a quality-oriented rule with false positives is a rule teams switch off. So <code>alt=\"IMG_2847.png\"<\/code> passes. So does the same <code>alt=\"3\/5 stars\"<\/code> on five different star-shaped icons.<\/p>\n<p class=\"wp-block-paragraph\">We built an <a href=\"https:\/\/github.com\/github\/accessibility-scanner-alt-text-plugin\">alt text plugin<\/a> for the <a href=\"https:\/\/github.com\/github\/accessibility-scanner\">GitHub Accessibility Scanner<\/a> 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.<\/p>\n<p class=\"wp-block-paragraph\">If you\u2019re building automated checks of your own, for accessibility or otherwise, the tradeoffs should transfer.<\/p>\n<h2 class=\"wp-block-heading\">Proving a string is wrong without seeing the picture<\/h2>\n<p class=\"wp-block-paragraph\">Presence of alt text is an objective fact; the attribute is there or it isn\u2019t. Quality is often a judgment call. A machine can\u2019t <em>prove<\/em> whether a sentence adequately describes a picture in context from markup.<\/p>\n<p class=\"wp-block-paragraph\">However, not all quality is subjective. There\u2019s several checks you can perform based on the alt text alone, with no need to consult the image content:<\/p>\n<ul class=\"wp-block-list\">\n<li>The attribute is absent (not empty) or whitespace-only.<\/li>\n<li>The alt is a filename, such as <code>hero.png<\/code>, <code>IMG_2847.jpg<\/code>.<\/li>\n<li>The alt is a placeholder somebody meant to replace, such as <code>TODO<\/code>, <code>tbd<\/code>.<\/li>\n<li>The alt is one generic word naming the medium instead of the content, such as <code>image<\/code>, <code>logo<\/code>, <code>chart<\/code>.<\/li>\n<li>The same alt repeats across adjacent images.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">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\u2019t support on its own.<\/p>\n<p class=\"wp-block-paragraph\">First, we had to determine which images to judge on a scanned webpage. We use Playwright\u2019s role-based locator rather than <code>querySelectorAll('img')<\/code>, so anything not included in the browser\u2019s <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Glossary\/Accessibility_tree\">accessibility tree<\/a> drops out, including anything carrying <code>alt=\"\"<\/code>. 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.<\/p>\n<p class=\"wp-block-paragraph\">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:<\/p>\n<ul class=\"wp-block-list\">\n<li><code>alt=\"image\"<\/code> gets flagged.<\/li>\n<li><code>alt=\"image of the login screen with the SSO button highlighted\"<\/code> doesn\u2019t.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">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.<\/p>\n<h2 class=\"wp-block-heading\">Repetition is a layout problem, not a DOM problem<\/h2>\n<p class=\"wp-block-paragraph\">Repeated alt text presented an interesting problem. Picture a row of five star-shaped icons that each say <code>\"3\/5 stars\"<\/code>. A screen reader user hears the same thing five times and learns nothing new from four of them.<\/p>\n<p class=\"wp-block-paragraph\">Our first version walked the images in document order and flagged any run sharing the same normalized alt. It caught things it shouldn\u2019t have. For example, a footer \u201cGitHub\u201d logo and a header \u201cGitHub\u201d 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.<\/p>\n<p class=\"wp-block-paragraph\">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:<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>const gap = Math.max(horizontalGap, verticalGap) \nconst largerDim = Math.max(a.boundingBox.width, a.boundingBox.height, \n                           b.boundingBox.width, b.boundingBox.height) \nreturn gap &gt; GAP_MULTIPLIER * largerDim<\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Two details worth noting:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong>The multiplier is a judgment call<\/strong>, not a number we derived from anything. It\u2019s the kind of value you tune against real pages instead of trusting from a spec.<\/li>\n<li><strong>When either image has no measurable box, the check fails open<\/strong> and the run continues. A missing finding is invisible; a wrong one isn\u2019t.<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">Getting a model to act like a reviewer, not a critic<\/h2>\n<p class=\"wp-block-paragraph\">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 <code>alt=\"a smiling person\"<\/code> is fine depends entirely on what surrounds it: on a generic mood shot, it\u2019s probably works. But under a heading where a specific person is named, it doesn\u2019t provide enough detail.<\/p>\n<p class=\"wp-block-paragraph\">In our optional <code>alt-text-quality<\/code>check, we extract page context alongside each image: the nearest heading, the page title, any <code>&lt;figcaption&gt;<\/code>, whether the image sits inside a link or button, and up to 600 characters of nearby prose.<\/p>\n<p class=\"wp-block-paragraph\">The link signal matters most, because when an image is a link\u2019s only content, its alt becomes the link\u2019s accessible name. The right alt then names the destination instead of describing the picture.<\/p>\n<p class=\"wp-block-paragraph\"><strong>One caution:<\/strong> The plugin only records that an image sits inside a link. We don\u2019t check whether it\u2019s the link\u2019s only content, which is the part that actually turns alt into a link name. So right now both cases look identical to the model.<\/p>\n<p class=\"wp-block-paragraph\">That context, the alt, and the image go to a vision model through <a href=\"https:\/\/github.com\/marketplace\/models\">GitHub Models<\/a>. 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 \u201ccould this be better?\u201d is a question a language model always answers yes to. Every image becomes a finding, so the signal disappears.<\/p>\n<p class=\"wp-block-paragraph\">Three changes fixed it:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong>A decision procedure instead of an instruction.<\/strong> The prompt walks four ordered steps, stops at the first that matches, and emits that step\u2019s verdict: decorative, redundant with a caption, functional, or informative.<\/li>\n<li><strong>Explicit anti-nitpick rules.<\/strong> Trust the author\u2019s framing. Separate redundant prefixes (\u201cImage of\u2026\u201d) from semantic ones (\u201cPhotograph of\u2026\u201d). Treat a short alt as <em>correct<\/em> when the surrounding prose already analyzes the image.<\/li>\n<li><strong>Structured output with a forced field order,<\/strong> so <code>reasoning<\/code> is generated before <code>verdict<\/code> and the model has to build an argument before it picks a label.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">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: <a href=\"https:\/\/webaim.org\/techniques\/alttext\/\">WebAIM<\/a>, the <a href=\"https:\/\/www.w3.org\/WAI\/tutorials\/images\/\">W3C images tutorial<\/a>, and <a href=\"https:\/\/poet.bornaccessible.org\/\">POET<\/a>. The rule and the harness share one prompt, so what you tune offline is what runs in CI. That harness only tests the model\u2019s judgment, though, not the whole pipeline. A case can score perfectly there and never reach the model in a real scan.<\/p>\n<h2 class=\"wp-block-heading\">Sending images to a model is a privacy and cost decision<\/h2>\n<p class=\"wp-block-paragraph\">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:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong>The rule is off by default.<\/strong> It won\u2019t run unless you deliberately enable it in your plugin configuration, and it needs a token with access to GitHub Models.<\/li>\n<li><strong>URLs get redacted.<\/strong> Image URLs and link <code>href<\/code>s often carry signed CDN tokens or session identifiers, so query and fragment are stripped from anything entering the model context or the rule\u2019s error logs. For the same reason, <code>src<\/code> and <code>srcset<\/code> are replaced with <code>(omitted)<\/code> in the markup we send.<\/li>\n<li><strong>Everything in that context window is untrusted input.<\/strong> Titles, headings, and prose all come from the page being scanned, and a page can contain text written to steer a model. Structured output constrains the shape of a response, not the reasoning behind it.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\"><strong>One caution, because that list is easy to over-read:<\/strong> findings still carry the real page URL and original HTML into the scanner\u2019s normal reporting pipeline. That\u2019s on purpose, since you can\u2019t fix an image you can\u2019t 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.<\/p>\n<p class=\"wp-block-paragraph\">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\u2019s reason enough to put it on a schedule rather than on every commit.<\/p>\n<h2 class=\"wp-block-heading\">What this still can\u2019t do<\/h2>\n<ul class=\"wp-block-list\">\n<li><strong>The deterministic rules are literal.<\/strong> They catch alt text that\u2019s obviously unwritten, not alt text that\u2019s fluent and wrong. They also read the <code>alt<\/code> attribute rather than the computed accessible name, so an <code>aria-label<\/code> that fixes the problem won\u2019t stop the finding.<\/li>\n<li><strong>The model-backed rule produces false positives.<\/strong> Every finding is a prompt for human attention, not a verdict.<\/li>\n<li><strong>Silence isn\u2019t coverage.<\/strong> That rule re-fetches images outside the browser session, so anything behind authentication can fail to load. Fetch and model errors are logged and skipped, which means a page can come back clean because nothing got checked.<\/li>\n<li><strong>Suggested alt text is a draft.<\/strong> A model that sees the image and a few nearby words can\u2019t account for your audience, your house style, or the job that image is doing on the whole page.<\/li>\n<li><strong>Some findings double up with the scanner\u2019s built-in checks<\/strong>, since our <code>missing-alt<\/code> rule covers the same ground.<\/li>\n<li><strong>We only check HTML<\/strong> <code>&lt;img&gt;<\/code> <strong>tags.<\/strong> SVG, <code>role=\"img\"<\/code> containers, CSS backgrounds, and canvas aren\u2019t covered yet.<\/li>\n<li><strong>This is new code with limited real-world feedback.<\/strong> Rules like these improve when they meet the variety of markup and content found across real sites. This plugin hasn\u2019t had that yet, so treat early findings accordingly.<\/li>\n<li><strong>Passing isn\u2019t conformance.<\/strong> Automated checks are a floor. Testing with people who use assistive tech is the goal.<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">What we\u2019d tell you if you\u2019re building something similar<\/h2>\n<p class=\"wp-block-paragraph\">Separate what you can prove from what you can only suspect, and give them different defaults. Checks that <em>prove<\/em> something should be cheap, predictable, and on by default. Checks that only <em>suspect<\/em> 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 <em>is<\/em> the link. We read an attribute, not a computed name.<\/p>\n<p class=\"wp-block-paragraph\">That distance is the real boundary, and a better model doesn\u2019t close it. Deciding what the functionality of an image is for a user who can\u2019t see it still requires human judgment. What automation buys you is making sure that human is giving the right images a second examination.<\/p>\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/github.com\/github\/accessibility-scanner-alt-text-plugin\">Try the alt-text plugin in your accessibility scanning workflow.<\/a> <\/strong>If it tells you the wrong thing, please report it. Open an <a href=\"https:\/\/github.com\/github\/accessibility-scanner-alt-text-plugin\/issues\">issue<\/a> with the finding and, if public, a link to the affected page.<\/p>\n<p>The post <a href=\"https:\/\/github.blog\/engineering\/user-experience\/your-alt-text-passes-automated-checks-that-doesnt-mean-its-any-good\/\">Your alt text passes automated checks. That doesn\u2019t mean it\u2019s any good.<\/a> appeared first on <a href=\"https:\/\/github.blog\/\">The GitHub Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>More than one in four images on the web\u2019s most popular home pages have alt text that\u2019s missing, vague, or [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":94,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[8],"tags":[],"class_list":["post-4914","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-github-engineering"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4914","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=4914"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4914\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/94"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=4914"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=4914"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=4914"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}