npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

a11y-why

v0.1.0

Published

Auditor de accesibilidad HTML que te explica POR QUÉ algo está mal, no solo qué. Sin navegador, sin configuración. / HTML accessibility linter that explains WHY, not just what. No browser, no config.

Readme

a11y-why

An HTML accessibility linter that explains why, not just what. No browser. No config. One dependency.

Léeme en español · Messages available in English and Spanish.

npx a11y-why index.html

Why another accessibility linter

There are good ones. They have gaps:

| Tool | Needs a browser | Explains the reason | Speed | |---|---|---|---| | axe-core | yes (needs a real DOM) | short reference text | fast once the browser is up | | pa11y | yes (spawns Chrome) | short reference text | slow, heavy in CI | | htmlhint | no | no | fast | | a11y-why | no | yes, at length | fast |

Two consequences of not needing a browser: it runs in about a second on a folder of files, and it works on HTML that never gets served — templates, partials, static output, files in a pull request diff.

And the part that gives the package its name: when it finds something, it tells you what a real person actually experiences because of it. Not a WCAG reference number to go look up. The reason.

Example

$ npx a11y-why index.html

index.html

  ✖  9:3     This <img> has no alt attribute.  img-alt
       <img src="photo-2847.jpg">

       Why:
       When a screen reader hits an image with no alt, it cannot skip it (it might
       matter) so it reads the only thing it has: the filename. The user hears "IMG
       dash 2 8 4 7 dot jpeg". Now picture a gallery with thirty of them.
       And mind the distinction, which is the whole point of this rule: omitting alt
       and writing alt="" are not the same. alt="" tells the reader "this is
       decoration, skip it silently" and is the correct answer for a spacer or an
       ornament. Omitting alt says nothing, so the reader has to improvise. One is a
       decision, the other is an oversight.

       Fix:
       If the image carries information: alt="Description of what it shows or does".
       If it is purely decorative: alt="" (empty, but present).

       WCAG 1.1.1 Non-text Content (A)
       https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html

✖ 17 errors   ⚠ 2 warnings

Each explanation prints once per run, no matter how many times the rule fires. Thirty images missing alt give you thirty one-line reports and one explanation, not thirty walls of text.

Usage

a11y-why index.html              # one file
a11y-why src                     # a folder, recursively
a11y-why "**/*.html"             # a pattern (expanded internally, so it works on Windows too)
cat page.html | a11y-why         # stdin

Options

| Flag | What it does | |---|---| | -l, --lang <es\|en> | Message language. Default: es | | -f, --format <name> | pretty (default), json, github | | --rule <id> | Run only this rule. Repeatable | | --exclude <id> | Skip this rule. Repeatable | | --brief | One line per issue, no explanations | | -q, --quiet | Errors only, hide warnings | | --strict | Exit 1 on warnings too | | --no-color | Disable colours | | --list-rules | List every rule and exit |

Exit codes: 0 clean · 1 errors found · 2 bad usage.

In CI

--format github prints GitHub Actions annotations, so issues land directly on the pull request diff at the right line:

- run: npx a11y-why "src/**/*.html" --format github

Rules

17 rules. All of them detectable in static HTML — see What this does not do for the honest limits.

| Rule | Level | Catches | |---|---|---| | doctype | error | Missing <!DOCTYPE html> (browser falls into quirks mode) | | meta-charset | error | No declared encoding (mojibake, and a UTF-7 XSS vector) | | html-lang | error | <html> with no lang (screen reader uses the wrong voice) | | doc-title | error | Missing or empty <title> | | viewport-zoom | error | user-scalable=no or maximum-scale under 2 | | img-alt | error | <img> with no alt; warns on useless alt text | | link-name | error | Links with no readable text | | button-name | error | Empty buttons, icon-only buttons with no label | | iframe-title | error | <iframe> with no title | | input-label | error | Fields with no label; flags placeholder-as-label | | heading-order | warning | Skipped heading levels, page not starting at h1 | | list-structure | error | Non-<li> children inside a list | | th-scope | error | Missing scope in tables with headers in both directions | | duplicate-id | error | Repeated ids (silently breaks for and aria-labelledby) | | positive-tabindex | error | tabindex greater than 0 | | fake-interactive | error | <div> with onclick or cursor:pointer — unreachable by keyboard | | fake-heading | warning | <div> styled to look like a heading but announced as a paragraph |

Programmatic API

import { audit } from 'a11y-why';

const report = audit(html, { lang: 'en', file: 'index.html' });

report.errorCount;        // 17
report.issues[0].ruleId;  // 'img-alt'
report.issues[0].why;     // the long explanation
report.issues[0].fix;     // how to fix it
report.issues[0].line;    // 9

Full TypeScript types included. Rules are exported individually and the rule format is public, so you can write your own:

import { defineRule } from 'a11y-why';

What this does not do

Being clear about the limits, because a tool that oversells makes people think they are covered when they are not:

  • No colour contrast. That needs computed CSS, which needs a browser. If you need it, axe-core or pa11y is the right tool — this one complements them, it does not replace them.
  • No JavaScript-generated content. It reads the HTML you wrote, not the DOM after your framework has run. For a SPA, lint the components' output.
  • No full ARIA validation. It understands aria-label, aria-labelledby, aria-hidden and interactive roles. It does not validate every role and attribute combination in the spec.
  • Only inline style is read, and only for fake-heading and fake-interactive. External stylesheets are invisible from here.

Passing this linter does not mean your site is accessible. No automated tool can tell you that — roughly a third of WCAG criteria are machine-checkable at all. It means the mechanical mistakes are gone, which is the floor, not the ceiling. The rest needs a keyboard, a screen reader and real people.

Requirements

Node 18.17 or newer. Single runtime dependency: parse5, the spec-compliant HTML parser that jsdom uses — so the tree this audits is the same tree your browser builds, including its quirks.

Licence

MIT