Skip to content

VelkinaStudio/altpilot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

altpilot

Audit a website, a folder, or your codebase for image accessibility — find missing and low‑quality alt text before a screen‑reader user does.

altpilot is a zero‑dependency CLI + library that scans .html, .jsx, .tsx, .vue, .svelte, and .astro files (or a live URL) for <img>, Next.js <Image>, and JSX *Image components, then reports:

  • Missing alt — the single most common WCAG failure.
  • Empty alt that isn't decorativealt="" is correct only for decorative images.
  • Low‑quality alt — filename‑as‑alt (DSC_0042.jpg), redundant prefixes ("image of…"), placeholder words ("image", "logo"), and over‑long alt.

It exits non‑zero when it finds errors, so it drops straight into CI as a gate. No build step, no config file, no API key required.

$ altpilot ./public
  ✖:13 <img> No alt attribute. Add alt text describing the image… [missing-alt]
      src: /img/hero.png
  ✖:22 <img> Alt text looks like a filename or slug… [filename-as-alt]
      src: /img/DSC_0042.JPG
  ▲:25 <img> Alt starts with "image of" / "photo of"… drop the prefix. [redundant-prefix]

4 errors  ·  4 warnings  ·  1 info  ·  2/10 clean
$ echo $?
1

Why this matters (and why these rules)

About 1 in 4 U.S. adults lives with a disability, and screen‑reader users depend on alt text to know what an image conveys. When alt is missing, most screen readers fall back to announcing the filename — so DSC_0042.jpg gets read out, character by character. When alt is bad ("image", "photo of a cat", the filename echoed back), it's noise that wastes the user's time without conveying meaning.

This is codified in WCAG 2.2 Success Criterion 1.1.1 — Non‑text Content (Level A): all non‑text content must have a text alternative that serves the equivalent purpose. The hard part isn't the rule — it's that the alt attribute can be present and still be useless, and a type‑checker or HTML validator won't catch that. altpilot grades quality, not just presence, following the W3C WAI Alt‑Text Decision Tree and WebAIM's Alternative Text guidance.

Note: automated tooling can verify mechanics (is alt present? is it a filename? is it decorative‑but‑described?). It cannot verify that the alt accurately describes the image — that's human judgment about the image's purpose in context. altpilot is honest about this line and never pretends to cross it.


Install

# one-off, no install
npx @velkina/altpilot ./public

# project dev dependency (recommended for CI)
npm install --save-dev @velkina/altpilot

# global
npm install -g @velkina/altpilot

Requires Node 18+ (uses native fetch for URL scanning). Zero runtime dependencies.


Usage

altpilot <path-or-url> [options]
altpilot ./public                 # scan a directory
altpilot index.html               # scan a single file
altpilot src/components --json    # JSON output for tooling/CI
altpilot https://example.com      # scan a live URL's HTML
altpilot ./src --strict           # treat warnings as failures too
altpilot ./public --suggest       # draft alt-text suggestions (see below)

Options

Option Description
--json Emit a structured JSON report instead of text.
--strict Treat warnings as failures (exit 1 on warnings too).
--suggest Draft alt suggestions for missing/weak images (offline by default).
--no-color Disable ANSI colors.
--ext <list> Comma‑separated extensions to scan (e.g. --ext .html,.tsx).
--quiet Print only the one‑line summary.
--help, -h Show help.
--version, -v Show version.

Exit codes (so it works as a CI gate)

Code Meaning
0 Clean — no errors (warnings allowed unless --strict).
1 Accessibility errors found (or warnings under --strict).
2 Usage / runtime error (bad path, network failure).

The heuristics

Each rule maps to WCAG 1.1.1 and carries a stable rule id (so output is greppable and you can suppress per‑rule in your own tooling).

Rule Severity Triggers when… Why
missing-alt error No alt attribute at all. Screen readers fall back to reading the filename. Hard WCAG 1.1.1 failure.
empty-alt-non-decorative warning alt="" but the image isn't marked role="presentation"/aria-hidden. Empty alt is correct only for decorative images; otherwise it hides meaning.
filename-as-alt error Alt equals the src basename, ends in an image extension, or matches a device pattern (DSC_0042, IMG_2031, 20231101-120000), or is a slug (hero-banner-final-v2). A filename is not a description. This is the most common "present but useless" failure.
placeholder-alt error / warning Alt is a single low‑info word. "image", "photo", "thumbnail"error. "logo", "icon", "banner", "avatar" → warning (they often just need brand/context). "image" conveys nothing; "logo" needs which brand.
redundant-prefix warning Alt starts with "image of", "photo of", "picture of", "graphic of", etc. Screen readers already announce it as an image — the prefix is repeated noise. (WebAIM)
alt-too-long warning Alt is longer than 125 characters. Long descriptions belong in body text, a <figcaption>, or aria-describedby. 125 is the widely‑cited soft ceiling (historic JAWS truncation point).
decorative-ok info alt="" on an image correctly marked decorative. Confirmation, not a problem — surfaced so you can see it was intentional.
dynamic-alt info JSX alt={expr} — value can't be known statically. Reported, never failed: ensure the runtime value is descriptive.
spread-alt info JSX <img {...props} /> with no literal alt. Alt may be supplied via the spread; verify it always is.

Calibration choices (and their trade‑offs)

  • logo/icon are warnings, not errors. alt="Acme Corporation logo" is fine; bare alt="logo" usually isn't, but it's not a hard failure — so it warns rather than blocking CI.
  • 125 chars is a soft limit (warning). There is no hard WCAG number; concision is a recommendation, not a rule, so over‑long alt never fails the build by itself.
  • Comments and <img> text inside them are ignored. The scanner blanks //, /* */, JSX {/* */}, and <!-- --> comments before matching, so example markup in comments doesn't produce false positives.
  • Dynamic and spread alts never fail CI. alt={caption} and {...props} are surfaced as info, because their value isn't statically knowable — altpilot won't block you on something it can't actually see.

Use as a library

import { scan } from '@velkina/altpilot';

const report = await scan('./public');

console.log(report.summary);
// { images: 42, clean: 30, errors: 5, warnings: 7, infos: 0, byRule: {...} }

if (report.summary.errors > 0) {
  process.exit(1);
}

Lower‑level building blocks are exported too:

import { analyzeImage, extractImages } from '@velkina/altpilot';

extractImages('<img src="/x.png">', 'html');
// [{ tag: 'img', src: '/x.png', alt: null, altPresent: false, line: 1, ... }]

analyzeImage({ alt: 'DSC_0042.jpg', altPresent: true, src: '/img/DSC_0042.jpg' });
// [{ rule: 'filename-as-alt', severity: 'error', message: '…', wcag: '1.1.1' }]

CI usage

GitHub Actions

# .github/workflows/a11y.yml
name: a11y
on: [push, pull_request]
jobs:
  alt-text:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npx @velkina/altpilot ./src --strict

The step fails the build when errors are found (or warnings, under --strict). Use --json if you want to upload a machine‑readable artifact:

npx @velkina/altpilot ./src --json > altpilot-report.json

pre-commit hook

# .husky/pre-commit
npx @velkina/altpilot ./public || {
  echo "Fix the alt-text issues above before committing." >&2
  exit 1
}

--suggest mode (optional, pluggable, no key bundled)

--suggest drafts candidate alt text for missing/weak images. The core tool is fully useful without it.

By default it runs offline: it derives a scaffold from the filename (golden-retriever-puppy.jpg"Golden retriever puppy"), clearly marked low‑confidence and "NOT from image content — verify before using." When a filename carries no meaning (DSC_0042.jpg), it says so honestly rather than inventing detail.

  ✖:13 <img> No alt attribute… [missing-alt]
      src: /img/golden-retriever-puppy.jpg
      suggest: alt="Golden retriever puppy"  (scaffold, confidence: low)
      note: Draft derived from the filename only — NOT from image content. Verify before using.

To get real, content‑aware descriptions, register a vision provider. altpilot ships no SDK and no hardcoded key — you wire your own and it's read from the environment, degrading to the offline scaffold if the key is absent or the call fails:

import { registerProvider, createSuggester } from '@velkina/altpilot';

// Contract: async (input, env) => { text, confidence, source? }
//   input = { src, tag, file, context }
registerProvider('openai', async (input, env) => {
  // your call to a vision model using env.OPENAI_API_KEY …
  return { text: 'A golden retriever puppy chewing a tennis ball on grass', confidence: 'high' };
});

const suggest = createSuggester();          // auto-detects provider from env
const out = await suggest({ src: '/img/puppy.jpg' });

Provider selection: set ALTPILOT_SUGGEST_PROVIDER=openai (or rely on auto‑detection from a present OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY among registered providers). If nothing is configured, you get the offline scaffold. altpilot never auto‑writes suggestions into your files — alt text is human‑approved by design.


What altpilot does not do (honest limitations)

  • It does not judge whether your alt text is accurate. It can tell alt="DSC_0042.jpg" is a filename; it cannot tell whether alt="A cat" is true of the image. That requires seeing the image (use --suggest with a vision provider) and, ultimately, human judgment.
  • It is a static scanner, not a browser. For a live URL it reads the server‑rendered HTML; images injected later by client‑side JavaScript won't be seen. For SPAs, scan the source files instead.
  • It checks <img>, <Image>, <image>, and *Image components. It does not yet check CSS background-image, <svg> <title>/role, <input type="image">, <area>, aria-label on icon buttons, or <picture>/<source> nuances. These are tracked as future work.
  • String literals containing <img> are not stripped (only comments are). A <img> inside a runtime string template is a rare and benign false‑positive source.

These are deliberate scope lines, not hidden gaps. PRs that extend coverage are welcome.


Development

git clone https://github.com/VelkinaStudio/altpilot.git
cd altpilot
node --test        # run the test suite (no install needed — zero deps)
node bin/altpilot.js test/fixtures/sample.html   # try it on the fixtures

License

MIT © Velkina Studio

About

Accessibility auditor for image alt text (WCAG 1.1.1) — CLI + library, CI gate

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors