The Alt-Text Plugin is a plugin for the AI-powered Accessibility Scanner that flags low-quality alt text on images. It complements axe-core's built-in image-alt rule, helping teams ship images with descriptive, screen-reader-friendly alternative text.
The plugin helps teams:
- 🖼️ Catch vague, generic, or filler
alttext ("image","photo","icon") - 📛 Catch raw filenames used as
alttext ("IMG_1234.png") - 🔁 Catch runs of adjacent images that share the same
alttext - 🚧 Catch boilerplate placeholder
alttext ("todo","tbd","fixme") - ♿ Catch images missing an
altattribute entirely (without flagging intentional decorativealt="")
⚠️ Note: This plugin is in active development alongside the a11y scanner's public preview. New rules are still being added and end-to-end integration with the scanner's issue-filing workflow is still maturing. Always review filed issues before acting on them.
The plugin inherits the a11y scanner's general FAQ — see the link above for questions about scanning, caching, GitHub Enterprise, and more. Plugin-specific questions are answered inline below.
This plugin catches low-quality alt text that axe-core's built-in image-alt rule cannot — vague single-word alt, raw filenames, runs of duplicate alt, and never-filled-in placeholders. The scope is intentionally narrow: deterministic, heuristic checks on <img> elements only. Non-<img> role="img" elements, decorative alt="", and hidden subtrees are filtered out before any rule runs.
The project is under active development alongside the scanner's public preview. Roadmap and open work live in this repo's Issues. See CONTRIBUTING.md for how to contribute, including local setup, expected checks, and PR conventions.
To use the Alt-Text Plugin, you'll need:
- The AI-powered Accessibility Scanner wired into a GitHub Actions workflow in your repository
- The plugin's source files copied into
./.github/scanner-plugins/alt-text-scan/in the repository that runs the scanner workflow (see Getting started) - Everything required to run the scanner itself (Actions enabled, Issues enabled, a
GH_TOKENPAT — see the scanner README for the full list)
This plugin is currently consumed from source: copy the plugin files into the repository that runs the scanner workflow.
To develop the plugin locally, you'll also need:
- Node.js matching the
enginesfield inpackage.json— currently^22.13.0 || ^24 || ^26 - npm (ships with Node)
Following the conventions in the scanner's PLUGINS.md, each plugin lives under ./.github/scanner-plugins/<plugin-name>/ in the repository that runs the scanner workflow. Drop the plugin's index.ts and supporting files into ./.github/scanner-plugins/alt-text-scan/.
.github/
└── scanner-plugins/
└── alt-text-scan/
├── index.ts
├── src/
├── schema/
└── config.json # optional
📚 Learn more
Add "alt-text-scan" to the scanner action's scans input. If you don't already set scans, keep "axe" in the list too — the scanner only runs Axe by default, and providing any value at all opts you out of that default.
name: Accessibility Scanner
on: workflow_dispatch
jobs:
accessibility_scanner:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # Required so the scanner can read your repo's scanner-plugins/ directory
- uses: github/accessibility-scanner@v3
with:
urls: |
https://example.com
repository: REPLACE_THIS/REPLACE_THIS
token: ${{ secrets.GH_TOKEN }}
cache_key: REPLACE_THIS
scans: '["axe", "alt-text-scan"]' # Add "alt-text-scan" to enable this plugin👉 Update all
REPLACE_THISplaceholders with your actual values. See the scanner's Action inputs for the full list.
📚 Learn more
Trigger your scanner workflow manually or on its configured schedule. The plugin runs on every URL the scanner visits, extracts each image exposed to assistive technology, and emits a finding for every rule violation. The scanner then turns those findings into GitHub issues.
📚 Learn more
The plugin runs every extracted image through an append-only registry of rules. Each rule returns a finding when an image fails its criteria, and the scanner turns each finding into an issue.
| Rule | ID | Fires when | Example (flagged) |
|---|---|---|---|
| Missing alt | missing-alt-text |
The alt attribute is absent (null) or whitespace-only (" "). alt="" is treated as intentional decorative use and is not flagged. |
<img src="cat.png"><img src="cat.png" alt=" "> |
| Vague alt | vague-alt-text |
The alt text is one of a curated set of generic single words (image, photo, icon, logo, screenshot, chart, untitled, etc.) or short filler phrases (an image of, a photo of). Normalization is applied before matching: case-insensitive, whitespace-collapsed, surrounding punctuation stripped. |
<img alt="image"><img alt="An image of"><img alt="PHOTO."> |
| Filename as alt | filename-alt-text |
The alt text ends in a common image file extension (.png, .jpg, .jpeg, .gif, .svg, .webp, .bmp, .ico). |
<img alt="IMG_1234.png"><img alt="Screenshot 2024-04-28.jpg"> |
| Repeated alt | repeated-alt-text |
Two or more adjacent images on the rendered page share the same normalized alt text. Useful for patterns like five star icons all labeled "3/5 stars". |
Five consecutive <img alt="3/5 stars"> elements |
| Placeholder alt | placeholder-alt-text |
The alt text matches a known boilerplate string that signals it was never written (todo, tbd, fixme, placeholder, alt text, insert alt text, image alt). Normalization is applied before matching. |
<img alt="TODO"><img alt="insert alt text"> |
Before rules run, the plugin extracts images from the page through Playwright's accessibility tree (page.getByRole('img')) and narrows the result to actual <img> elements. The following are filtered out automatically and never reach the rules:
- Non-
<img>elements withrole="img"(e.g.<svg role="img">,<div role="img">) — this plugin's rules only apply to HTML<img>tags - Images inside
aria-hidden="true"subtrees - Images inside
display: noneorvisibility: hiddensubtrees - Decorative images with
alt=""(implicitrole="presentation")
The scanner's built-in Axe scan includes a rule called image-alt that catches missing and whitespace-only alt attributes. If you have both "axe" and "alt-text-scan" enabled, the same image may be flagged by both. The other four rules in this plugin (vague-alt-text, filename-alt-text, repeated-alt-text, placeholder-alt-text) are unique to the plugin and don't overlap with Axe.
When a rule fires, the plugin emits a finding with the following shape (matching the scanner's Finding type):
scannerType— always'alt-text-scan'; identifies which plugin produced the findingruleId— the ID of the rule that fired (e.g.'vague-alt-text')url— the page URL where the image was foundhtml— the offending<img>element's outer HTMLproblemShort— one-sentence description of what's wrong, including the offending alt text where applicableproblemUrl— link to the relevant WCAG technique or W3C tutorialsolutionShort— one-sentence description of how to fix itsolutionLong— optional longer explanation when one sentence isn't enough
The scanner uses these fields to file or update a GitHub issue.
To override the default enabled state of one or more rules, add a config.json file next to the plugin's index.ts in your scanner repository:
.github/scanner-plugins/alt-text-scan/
├── index.ts
└── config.json ← optional
{
"rules": {
"repeated-alt-text": false,
"placeholder-alt-text": false
}
}- Each key under
rulesis a rule ID from the Rules table above; the value istrue(run the rule) orfalse(skip it). - Rules you don't list keep their default behavior. Today every rule defaults to enabled.
- Unknown rule IDs and non-boolean values are logged as warnings and ignored (typo guard).
- A missing or malformed
config.jsoncauses the plugin to run with all defaults. - The plugin reads the config once at startup, not per URL.
A JSON Schema is published at schema/config.schema.json. Add a $schema line at the top of your config.json to get autocomplete, hover docs, and inline validation in editors that support JSON Schema (VS Code, JetBrains IDEs, etc.):
{
"$schema": "https://raw.githubusercontent.com/github/accessibility-scanner-alt-text-plugin/main/schema/config.schema.json",
"rules": {
"repeated-alt-text": false
}
}The $schema line is optional and is ignored by the plugin at runtime.
git clone https://github.com/github/accessibility-scanner-alt-text-plugin.git
cd accessibility-scanner-alt-text-plugin
npm ci| Script | What it does |
|---|---|
npm run test |
Runs the Vitest unit suite once |
npm run test:watch |
Runs Vitest in watch mode |
npm run typecheck |
Runs tsc --noEmit against the whole project |
npm run lint |
Runs ESLint |
npm run format |
Rewrites files with Prettier |
npm run format:check |
Reports formatting violations without writing |
Pull requests trigger two CI workflows: lint.yml runs lint and format:check on Node 24, and test.yml runs typecheck and test across Node 22, 24, and 26.
index.ts # Plugin entry point: exports `name` and the default scan function
src/
config.ts # Loads & validates .github/scanner-plugins/alt-text-scan/config.json
extract.ts # Pulls visible <img> records from a Playwright page
findings.ts # Translates each RuleResult into the scanner's Finding shape
rules/
index.ts # Append-only rule registry
missing-alt-text.ts
vague-alt-text.ts
filename-alt-text.ts
placeholder-alt-text.ts
repeated-alt-text.ts
utils/
normalize-alt-text.ts # Lowercase, trim, collapse whitespace, strip punctuation
types.ts # Rule, RuleContext, RuleResult, ImageRecord, Finding
tests/
extract.test.ts # Playwright-driven tests for the image extractor
example-site.test.ts # Runs the plugin against the example/site-with-errors fixture
fixtures/ # Static HTML fixtures used by the extractor tests
unit/
*.test.ts # One file per rule, plus config.test.ts for the loader
utils/
helpers.ts # makeImage() and evaluateAlts() — shared across rule tests
- Create
src/rules/<rule-name>.tsexporting aRule(seesrc/types.tsfor the shape —id,problemUrl, andevaluate(context)). Filenames undersrc/andtests/must be kebab-case; ESLint'scheck-file/filename-naming-conventionrule enforces this. - Import the rule in
src/rules/index.tsand append it toallRules. The registry is append-only — don't reorder existing rules. - Add a
tests/unit/<rule-name>.test.tsfile. UseevaluateAlts(alts, rule)andmakeImage(overrides)fromtests/utils/helpers.tsfor the common cases; construct an explicitRuleContextonly when you need control oversrcor other per-image fields. - Run
npm run test && npm run typecheck && npm run lintlocally before opening a PR. CI re-runs them.
Important
Image extraction happens once per page, before any rule runs, so every rule sees the same filtered list regardless of which rules are enabled. Don't reach into the DOM from a rule — work from the ImageRecord[] the rule's context provides.
💬 We welcome your feedback! To submit feedback or report issues, please open an issue in this repository. For broader feedback on the a11y scanner itself, file it in the scanner repository.
📄 This project is licensed under the terms of the MIT open source license. See the LICENSE file for the full terms.
🔧 Maintained alongside the AI-powered Accessibility Scanner. See CODEOWNERS for the responsible team.
❓ For support, please open an issue in this repository. See SUPPORT.md for support expectations, or the scanner's SUPPORT document for guidance that applies across the project.
✨ Built on top of Playwright, Vitest, and the broader open-source accessibility tooling ecosystem. Thank you to everyone contributing rules, tests, and review.