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

a11yfix

v0.1.0

Published

Accessibility linter that writes the patch, not just the report. WCAG 2.2 and EAA, from your source: JSX, Vue, Svelte, HTML, CSS. Zero dependencies.

Readme

A11yFix

CI npm node licence dependencies

Accessibility tools tell you what is broken. This one writes the patch.

npx a11yfix src --diff
--- a/src/Card.tsx
+++ b/src/Card.tsx
@@ -1,9 +1,9 @@
 export function Card({ title }: { title: string }) {
   return (
     <div className="bg-white p-6">
-      <h2 className="text-gray-400 text-2xl font-bold">{title}</h2>
-      <p className="text-gray-400">Supporting copy that is hard to read.</p>
-      <span className="text-emerald-500">Status: active</span>
+      <h2 className="text-gray-500 text-2xl font-bold">{title}</h2>
+      <p className="text-gray-500">Supporting copy that is hard to read.</p>
+      <span className="text-emerald-700">Status: active</span>
       <p className="text-gray-700">This passes and must be left alone.</p>
     </div>
   );

A run of a11yfix on demo/Card.tsx: three contrast errors with their measured ratios, and the line that says a clean run is not a conformance claim

That image is generated by running the command, not drawn — node scripts/gen-demo-svg.mjs, and CI fails if it stops matching what the command prints.

npm install -D a11yfix        # or run it with no install at all: npx a11yfix .

Node 20 or newer. Zero runtime dependencies, by design — a tool you point at your source should not bring a dependency tree with it.


Why this exists

Every accessibility checker — axe, Lighthouse, WAVE, Pa11y — inspects the rendered DOM. It tells you that a node on a page fails a contrast ratio. Then a human works out which component produced that node, finds the file, finds the colour, decides what to change it to, and checks the change did not break the design.

That translation step is most of the work, and no tool does it.

If the question you are actually asking is "why not just run axe", that is the right question and it has its own page: Where A11yFix sits among the tools you already have. Short answer: it does not replace any of them. Run axe. Add this when you want the findings turned into a patch.

A11yFix reads your source, keeps byte-exact offsets for every tag and attribute, and emits a patch. It scans 3,300 files in 1.3 seconds, needs no browser, no network, no API key, and no runtime dependencies at all.

What it actually fixes

Contrast repair is the centrepiece, because it is the most common failure on the web and the only common one with an arithmetic answer rather than a judgement call.

Given a foreground and a background, A11yFix finds the smallest perceptual change that reaches the required ratio. It works in OKLCH: hue is held fixed, lightness moves by the minimum needed, and chroma is only reduced if the result would fall outside sRGB.

It also picks the right side of the pair to move. White text on a mid-tone button gets fixed by darkening the button, not by turning the label charcoal — a naive fixer gets this wrong about half the time.

In a Tailwind codebase it swaps the shade rather than injecting a hex literal, walking the ramp until it finds a step that actually clears the threshold, and quotes the ratio that step really reaches. It composites bg-black/10 against what is behind it, resolves CSS custom properties and Tailwind v4 @theme tokens, and stops at a gradient or a background image instead of pretending the colour underneath is what you see.

Contrast is three of the 70 rules. The rest cover images and media alternatives, form labelling, document structure, keyboard operability, ARIA correctness, and link semantics. 59 of them name the WCAG success criterion they implement. The other 11 are things every practitioner will tell you to do that WCAG has no criterion for — a page with no <main>, an accesskey, a data table with no caption — and 10 of them are reported as warnings or information rather than as errors. The eleventh, A11Y-TODO-001, is not about the page: it reports a placeholder this tool itself wrote, and it is an error precisely so that an unfinished fix cannot pass as a finished one. docs/coverage.md says which rule is which.

One of those 11 is different in kind from the rest. A11Y-DOC-016 reports an accessibility overlay — accessiBe, UserWay, AudioEye, or the bvi.js «версия для слабовидящих» switch that is the usual answer to ГОСТ Р 52872-2019 — and says, as info and without asking you to remove anything, what it does not change: an image with no alt has no alt at any font size. On nine live Russian institutional sites, three carried a switch and all three had images with no alternative text behind it. See the field report.

What a run looks like

Every finding names who is affected, quotes the source, and says what the patch would do and what it would achieve.

demo/Card.tsx
  4:7  error    Text contrast is 2.54:1, below the required 3:1 (#9ca3af on #ffffff). WCAG 1.4.3
          People with low vision, colour vision deficiency, or anyone reading on a phone
          in sunlight cannot reliably make out this text.
          <h2 className="text-gray-400 text-2xl font-bold">
          <h2 className="text-gray-500 text-2xl font-bold">
          fixable (review): Replace text-gray-400 with text-gray-500, reaching 4.83:1.
          A11Y-COLOR-001

3 findings  (3 errors, 0 warnings)  across 1 file
0 fixable automatically, 3 fixable with review, 0 need a person.
Run with --fix to apply, or --diff to preview.

Two of the three findings are cut from that block and the impact sentence is wrapped to the width of this page; everything else is what the command prints.

The second <h2> line is the finding's own answer, not a description of it — the line as it would read after the patch, so the change can be checked by looking rather than by applying. And that "reaching 4.83:1" is recomputed from the shade the patch actually writes, not from the ideal colour the solver aimed at. It is a number you can check.

What it will never do

It does not invent alt text. It cannot see the image. A plausible-sounding wrong description is worse than a missing one: it lies to a screen-reader user and makes every downstream checker report the page as fixed. Same for link text, button labels, and lang. Those are reported and never patched: the finding stays, and the exit code stays 1, until a person writes the words.

Where a fix does leave a placeholder for someone to complete, A11Y-TODO-001 reports the placeholder itself as an error, so an unfinished fix cannot pass as a finished one.

It does not claim conformance. Automated testing reaches roughly a third of WCAG's success criteria. A clean run means the machine-checkable subset passes. Keyboard flows, focus order, meaningful alternative text and actual screen-reader behaviour still need a human. Every run prints this. See docs/coverage.md for the exact list of what is and is not covered.

It does not patch what it cannot prove. If a colour resolves through a runtime expression, if the element sits over a background image, or if a fix would need a change large enough to alter the design, A11yFix says so and leaves the file alone.

Fix safety

Every fix carries a safety level, and nothing is written without you asking.

| Level | Meaning | Applied by | |---|---|---| | automatic | One provably correct answer, independent of context | --fix | | review | Correct in the common case, worth a glance | --fix --include-review | | manual | Needs knowledge the tool does not have | never — advice only |

Usage

npx a11yfix .                          # report, change nothing
npx a11yfix src --diff                 # preview the patch
npx a11yfix src --fix                  # apply automatic fixes
npx a11yfix src --fix --include-review # apply reviewable fixes too
npx a11yfix . --json                   # machine-readable
npx a11yfix . --all                    # include info findings and repeats
npx a11yfix --rules                    # list all 70 rules
npx a11yfix . --baseline-write         # record existing findings, then gate on new ones
npx a11yfix . --baseline               # report only what is not in the baseline
npx a11yfix . --report                 # standalone HTML audit report

Exit code is 1 when any error-severity finding remains, so it works as a CI gate.

Adopting on a project that already has findings

The first run on an existing application is not going to be zero. A real one produced 1382 findings, and nobody fixes 1382 things before merging the next feature. Record them once and gate on the difference:

npx a11yfix . --baseline-write   # writes .a11yfix-baseline.json — commit it
npx a11yfix . --baseline         # in CI: exit 1 only on findings that are not in it

Findings are matched by rule, file and the shape of the code — whitespace collapsed, quotes unified — never by line number. Editing lines above a finding, or running the file through a formatter, does not make it look new. A second copy of an already-known violation is known; a third is new.

When findings in the baseline stop occurring, the run says so and suggests rewriting it, so the file shrinks as the codebase improves instead of quietly accumulating permission to regress.

The report

--report writes one self-contained HTML file: no scripts, no fonts, no images. It opens offline, prints, and survives being emailed. Each finding carries the file, the line and the exact source that triggered it, so every claim in it can be checked in under a minute — and the limits of what a source analyser can see are stated before the findings rather than after.

npx a11yfix . --report                 # a11yfix-report.html
npx a11yfix . --report audit.html --lang ru

It groups findings by rule, names the file and line for every one, quotes the source that triggered it, and links each rule to the W3C's own page for the criterion it belongs to. By default it lists the first twelve occurrences of each rule and says how many it left out; --report --all lists every one, which is what you want when the file is going to somebody as a deliverable rather than being skimmed.

It states what the tool cannot check before the findings rather than after. A report that lets a passing automated scan imply conformance is the thing the FTC fined an overlay vendor for in 2025, and burying the caveat at the bottom is how that happens.

Where the change is one a11yfix would write, the report shows it as two lines rather than describing it:

now           <a href="https://shop.example.ru/" target="_blank" class="link">
after the     <a href="https://shop.example.ru/" target="_blank" class="link" rel="noopener">
change

That is the whole deliverable when the scan came from a URL. There is no repository to send a diff to, and a content editor cannot apply one; they can retype a line. The label says "after the change" rather than "correct" because some patches are a floor and not a finish — role="button" tabindex="0" makes an element focusable and still needs a key handler — and the sentence directly under each pair says which is which. A finding only a person can settle, like a missing alt, never gets a second line: inventing one is the thing this tool exists not to do.

--lang ru translates the report, not the terminal: the person who runs the command reads English compiler output all day, and the person the report is handed to may not. Every rule has Russian text and a test fails the build if one is added without it. Where a patch is offered, the change itself — text-gray-400 → text-gray-600 — is printed in both languages, because that is the line a reader checks against their own file.

The Russian report also names each criterion the way ГОСТ Р 52872-2019 names it — «1.4.3 Контрастность (минимальные требования) (п. 4.1.4)» — because that is the wording in the contract the reader is holding. No correspondence had to be invented: the standard was written from WCAG 2.1 and kept its numbering exactly, so the mapping is an identity and all 78 criteria are transcribed from the standard's own text. It still does not issue a conformance statement, and docs/gost.md says which 19 of the 78 the tool reaches and why the other 59 need a person.

Scanning a live page

Not everyone who needs this has a repository to hand you. A library, a school, a museum whose site an agency built in 2019 has a URL and nothing else, so a URL is enough:

npx a11yfix https://example.ru/
npx a11yfix https://example.ru/ --report audit.html --lang ru

It reads the HTML the server sends and the stylesheets that page links from its own origin. It is not a crawler and not a browser — one page, no JavaScript. That last part is the thing to understand rather than work around: a client-rendered application serves a nearly empty shell to anything that is not a browser, every rule then finds nothing, and a run like that must never read as a pass. So it says so:

read https://www.msu.ru/
  the served HTML is nearly empty — this page is assembled by JavaScript
  in the browser, so almost nothing here can be checked from the source.
  Scan the repository instead, or a URL the server renders in full.

The report carries the same caveat, because the person holding it cannot otherwise tell which of the two kinds of scan produced it. --fix, --diff and --baseline-write are refused for a URL: there is no file to change, and a diff against a copy the owner does not have is worse than no diff.

Encodings are honoured, including windows-1251, which is still in use on the sites this mode exists for — a page decoded as the wrong charset would have every text rule reporting on mojibake.

A whole site, from its own sitemap

npx a11yfix --sitemap https://example.ru/sitemap.xml --report audit.html --lang ru

Fifty pages by default, one at a time with a pause between them; --max-files moves the cap. A sitemap index is followed one level down.

The fifty are spread evenly across the list rather than taken from the front of it. A sitemap comes out in whatever order the CMS emitted it, which is usually oldest section first: on one museum site the first twelve entries include 1script.php, script.php and test.php. Reading the front of the list produces an audit of one corner of the site and calls it an audit of the site. The report says what it sampled and from how many, and the front page is always read even when the sitemap forgets to list it.

The report then opens with every page it read, worst first — which is the coverage the reader is owed and, incidentally, the order to work through. A page assembled in the browser is marked as one, so its low count is not mistaken for a good one.

Configuration

.a11yfixrc.json in the project root, or an a11yfix key in package.json. A command-line flag always wins over the file.

{
  "level": "AA",
  "ignore": ["apps/web/public/**", "**/*.stories.tsx"],
  "rules": { "A11Y-LINK-007": "off" }
}

The same thing from the command line: --ignore "apps/web/public/**" --disable A11Y-LINK-007.

For a single justified exception, a comment in the file beats turning the rule off everywhere:

<!-- a11yfix-disable-next-line A11Y-IMG-001 -->
<img src="divider.png">

a11yfix-disable-line and a11yfix-disable-file work too, in HTML comments and in JSX {/* … */} comments. Naming no rule suppresses every rule on that line. There is deliberately no "disable from here on" — a blanket switch buried mid-file is indistinguishable from the tool being broken, and nobody ever finds it again.

Suppressions that stop matching anything are reported by file and line, so they cannot quietly outlive the problem they were written for.

Every run prints which config it used, how many files were ignored and how many rules are off, so a finding that never appears can be traced to the line that silenced it.

Supported sources: .html .jsx .tsx .vue .svelte .astro, with .css/.scss read for colour resolution. .ts and .js are deliberately not parsed as markup — they usually contain HTML only inside string literals, and reporting on those is noise.

GitHub Action

- uses: iam-artificis/a11yfix@v1     # or @v0.1.0 to pin exactly
  with:
    path: src
    mode: comment   # or: fail | pr

v1 tracks the action's inputs, not the package version: it moves when path, mode, level or token change, which is not on every release. Pin @v0.1.0 if you would rather nothing move at all, and set the version input to pin the npm package with it.

comment posts the findings on the pull request and updates the same comment on every push, fail gates the build, pr opens a follow-up pull request containing the fixes. pr never pushes to the branch under test.

Verified, not asserted

Four properties are enforced by the test suite rather than claimed here:

  • Every diff applies. Generated patches are round-tripped through real git apply and compared byte-for-byte against the direct edit — LF and CRLF, with and without a trailing newline, single-hunk and multi-hunk.
  • Every contrast value matches the WCAG reference. The colour maths is checked against published ratios, and OKLCH conversion round-trips exactly.
  • Every ratio a fix claims to reach is the ratio it reaches. The number in the report is recomputed from the palette step actually written, not from the solver's ideal.
  • No fix invents text. A machine check asserts that nothing written into an alt, aria-label, title or lang position is anything but an empty value or a marked TODO.

Precision

Tuned to miss a real issue rather than invent one, because a false positive costs a developer an afternoon and their trust in everything else the tool said.

Measured against public repositories at the time of writing:

| Repository | Files | Errors | Warnings | |---|---|---|---| | vercel/commerce | 45 | 4 | 0 | | tailwindlabs/tailwindcss.com | 150 | 26 | 14 | | documenso/documenso | 674 | 52 | 14 | | calcom/cal.com | 989 | 828 | 287 | | shadcn-ui/ui | 3334 | 100 | 65 |

Getting there meant fixing five classes of confident, wrong finding that only real code produced — a discarded Tailwind opacity modifier reading bg-green-500/10 as solid green, a stylesheet in one package colouring pages in another, <Html> from an email library treated as the document root, markup inside a dedent block treated as a page. Each is now a regression test. The full write-up, with pinned commits and the cases where the tool is still wrong, is in docs/field-report.md.

Licence

Apache-2.0.