a11ymedicaccessibility clinic

Free accessibility checker for WCAG 2.2

The accessibility checker that explains every problem and writes the fix.

Bring any page in for a check-up. a11ymedic examines it with 61 WCAG 2.2 rules, tells you in plain language what is wrong and who it hurts, and hands you the patched HTML. It works as a page overlay, on the command line, in CI and inside your tests.

Start a check-up
  • 61WCAG 2.2 rules
  • EN + PTplain language
  • OKLCHcolor fixes
  • 1 diffsafe auto fix
Patch, the a11ymedic otter paramedic, in his clinic. A sick browser window sits on the stretcher with a thermometer, glasses for low vision and bandaged buttons, next to a heart monitor, an eye chart that reads A11Y MEDIC and an IV bag of alt text. EA11YMEDIC 72WCAG 2.2 alt
Patient: one browser window. Symptoms: no alt text, mystery buttons, pale grey text.

Who it is for

Every diagnosis names who it hurts

A finding is not a lint warning. Each one says which people hit the barrier, so you know why the fix matters before you write it.

  • Screen reader usersHear the page read aloud and move by headings, links and landmarks.
  • Low visionZoom to 200 percent and beyond, and need strong contrast to read.
  • Color blindnessCannot tell some colors apart, so color alone says nothing.
  • Keyboard onlyTab, Enter and arrows instead of a mouse, so focus must show.
  • Cognitive and learningNeed clear labels, steady layouts and time to read.
  • Motor and dexterityTremors or a switch device make tiny targets and drags hard.
  • Deaf and hard of hearingRely on captions and transcripts for anything spoken.

Live demo

The check-up room

Pick a patient or paste your own HTML and run the medic. Numbered pins land on the page, the chart explains each problem with the exact fix, and Treat applies it to the live page so you can watch the patient recover.

Patient intake

Sample patient
Report language

0health

ConditionWaiting

The patient page runs in a sandboxed frame with no scripts. Pins are buttons: select one to open its diagnosis.

Diagnosis chart

Not examined yet
  • 0critical
  • 0serious
  • 0moderate
  • 0minor

  1. Why it matters

    Who is affected

    WCAG

    The fix

Contrast eye exam

Find the closest accessible color pair

Pick a text color and a background and read the chart. a11ymedic keeps the hue and moves only the OKLCH lightness until the pair passes, so your brand still looks like your brand. The same math writes the color-contrast fix in every report.

Large text, 24px

Normal body text at 16px. The quick brown otter jumps over the lazy bug.

4.5:1 contrast

  • AA normal text
  • AA large text
  • AAA normal text

Type a CSS color: #0f8b8d, rgb(), hsl(), oklch() or a name.

Prescription lenses

Change the text

Aa

on , :1

Change the background

Aa

on , :1

Change both

Aa

on , :1

Perfect vision: this pair already passes the target.

Read the guide: How to fix color contrast without losing your brand.

How it works

Exam, diagnosis, treatment

  1. 1 Exam

    An engine written from scratch runs 61 rules on a real DOM. In the browser it reads computed styles and real boxes. On Node it parses HTML with jsdom, cascades your CSS itself (specificity, variables, inheritance) and keeps line numbers.

  2. 2 Diagnosis

    Every finding says what is wrong with this exact element, why it matters, who is affected and which WCAG 2.2 criterion it breaks, in English and Portuguese, with the selector and the HTML snippet.

  3. 3 Treatment

    Each finding comes with patched code: the missing alt, aria-label or label for, a contrast safe color picked in OKLCH. Safe fixes apply in one command with a diff preview, and running them twice changes nothing.

Quick start

Four ways to call the medic

Same rules, same explanations and the same fixes wherever you run it.

House call

On any page: script tag or bookmarklet

Add one script tag in development. Numbered pins mark each problem and a keyboard accessible side panel explains it. Alt + Shift + A toggles it.

index.html
<script src="https://cdn.jsdelivr.net/npm/a11ymedic@0.1.0/dist/a11ymedic.js" data-overlay></script>
<!-- Portuguese: data-overlay="pt" -->

Or drag this bookmarklet to your bookmarks bar and press it on any site: a11ymedic check-up

Walk-in clinic

Command line: files, globs and URLs

Check built HTML, a folder or a live URL. With Playwright or Puppeteer installed, URLs are checked in a headless browser; without them the HTML is fetched and checked statically.

Terminal
npx a11ymedic "dist/**/*.html"
npx a11ymedic https://example.com --format html -o report.html
npx a11ymedic fix "src/**/*.html"          # preview safe fixes as a diff
npx a11ymedic fix "src/**/*.html" --write  # apply them

Daily rounds

CI: fail the build on serious problems

On GitHub Actions a11ymedic prints annotations on the changed lines and writes a job summary on its own.

.github/workflows/a11y.yml
name: Accessibility
on: [push, pull_request]
jobs:
  a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build
      - run: npx a11ymedic "dist/**/*.html" --fail-on serious

Lab tests

Tests: Vitest and Jest

Audit rendered components and get a readable failure with the fix for each problem.

button.test.ts
import { audit } from "a11ymedic";
import "a11ymedic/vitest"; // Jest: setupFilesAfterEnv: ["a11ymedic/jest"]

test("the toolbar is accessible", async () => {
  document.body.innerHTML = renderToolbar();
  expect(await audit(document.body)).toHaveNoViolations();
});

Install from npm

Terminal
npm install --save-dev a11ymedic

With your framework

React: Toolbar.test.tsx
import { render } from "@testing-library/react";
import { audit } from "a11ymedic";
import "a11ymedic/vitest";

test("Toolbar", async () => {
  const { container } = render(<Toolbar />);
  expect(await audit(container)).toHaveNoViolations({ severity: "serious" });
});
Vue: Card.spec.ts
import { mount } from "@vue/test-utils";
import { audit } from "a11ymedic";
import "a11ymedic/vitest";

it("Card", async () => {
  const wrapper = mount(Card, { props: { title: "Plans" }, attachTo: document.body });
  expect(await audit(wrapper.element)).toHaveNoViolations();
});
Angular: card.component.spec.ts
import { audit } from "a11ymedic";

it("is accessible", async () => {
  const fixture = TestBed.createComponent(CardComponent);
  fixture.detectChanges();
  const result = await audit(fixture.nativeElement);
  expect(result.findings).toEqual([]);
});
Next.js: app/layout.tsx (development only)
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        {process.env.NODE_ENV === "development" && (
          <Script src="https://cdn.jsdelivr.net/npm/a11ymedic@0.1.0/dist/a11ymedic.js" data-overlay />
        )}
      </body>
    </html>
  );
}

Rule catalog

61 rules mapped to WCAG 2.2

The medicine cabinet, sorted into twelve drawers. Every rule has a passing and a failing test fixture. The ones with a bandage are applied by a11ymedic fix --write; the others come with a suggested patch to review.

Medicine cabinet

61 of 61 rules

Images Alt text for pictures, icons, image maps and embedded objects. 6 rules
  • image-altcritical Image without alt text WCAG 1.1.1 Level A auto fix
  • image-alt-qualitymoderate Alt text that does not help WCAG 1.1.1 Level A auto fix
  • input-image-altcritical Image button without text WCAG 1.1.1, 4.1.2 Level A
  • area-altcritical Image map area without text WCAG 1.1.1, 2.4.4, 4.1.2 Level A
  • role-img-altserious Graphic without a name WCAG 1.1.1 Level A
  • object-altserious Embedded object without a text alternative WCAG 1.1.1 Level A
Names Every button, link, dialog, widget and frame needs a name people can hear. 8 rules
  • button-namecritical Button without a name WCAG 4.1.2 Level A
  • link-nameserious Link without a name WCAG 2.4.4, 4.1.2 Level A
  • link-text-genericmoderate Link text like "click here" WCAG 2.4.4 Level A
  • label-in-nameserious Visible label missing from the accessible name WCAG 2.5.3 Level A
  • aria-dialog-nameserious Dialog without a name WCAG 4.1.2 Level A auto fix
  • aria-widget-nameserious Custom control without a name WCAG 4.1.2 Level A auto fix
  • frame-titleserious Frame without a title WCAG 4.1.2 Level A
  • frame-title-uniqueminor Frames with the same title WCAG 4.1.2 Level A
Forms Labels, autocomplete, grouped radios and paste that works. 5 rules
  • labelcritical Form field without a label WCAG 4.1.2, 1.3.1, 3.3.2 Level A
  • autocomplete-validserious Invalid autocomplete value WCAG 1.3.5 Level AA auto fix
  • input-purposeminor Personal data field without autocomplete WCAG 1.3.5 Level AA
  • paste-blockedserious Pasting into a field is blocked WCAG 3.3.8 Level AA auto fix
  • radio-groupmoderate Radio buttons without a group label WCAG 1.3.1 Level A
Contrast Text that is bright enough against its real background. 2 rules
  • color-contrastserious Text with low color contrast WCAG 1.4.3 Level AA
  • color-contrast-enhancedminor Text contrast below the enhanced level (AAA) WCAG 1.4.6 Level AAA
Keyboard Focus you can see, a sensible tab order and no mouse only controls. 7 rules
  • tabindexserious Positive tabindex changes the focus order WCAG 2.4.3 Level A auto fix
  • focus-visibleserious Focus indicator removed WCAG 2.4.7 Level AA
  • nested-interactiveserious Interactive element inside another one WCAG 4.1.2 Level A
  • keyboard-click-handlerserious Clickable element that keyboards cannot use WCAG 2.1.1, 4.1.2 Level A
  • link-as-buttonmoderate Link used as a button WCAG 2.1.1, 4.1.2 Level A
  • bypassserious No way to skip repeated content WCAG 2.4.1 Level A
  • skip-linkmoderate In-page link points nowhere WCAG 2.4.1 Level A
Zoom and touch Pinch zoom stays on and targets are big enough to hit. 2 rules
  • meta-viewportcritical Zoom is disabled WCAG 1.4.4 Level AA auto fix
  • target-sizeserious Click or tap target smaller than 24 by 24 pixels WCAG 2.5.8 Level AA
Structure Titles, headings, landmarks and lists that screen readers can navigate. 11 rules
  • document-titleserious Page has no title WCAG 2.4.2 Level A auto fix
  • heading-ordermoderate Heading level skipped WCAG 1.3.1 Level A
  • page-has-heading-onemoderate Page has no main heading (h1) WCAG 1.3.1, 2.4.6 Level A
  • empty-headingmoderate Empty heading WCAG 1.3.1, 2.4.6 Level A
  • landmark-one-mainmoderate Page needs exactly one main landmark WCAG 1.3.1 Level A
  • landmark-uniquemoderate Landmarks that cannot be told apart WCAG 1.3.1 Level A
  • regionmoderate Content outside of landmarks WCAG 1.3.1 Level A
  • listserious List with invalid children WCAG 1.3.1 Level A
  • listitemserious List item outside of a list WCAG 1.3.1 Level A
  • definition-listserious Definition list with invalid structure WCAG 1.3.1 Level A
  • duplicate-idminor Duplicate id WCAG 4.1.2, 1.3.1 Level A
Language The right lang so words are read with the right voice. 3 rules
  • html-has-langserious Page language is not set WCAG 3.1.1 Level A auto fix
  • html-lang-validserious Page language code is invalid WCAG 3.1.1 Level A auto fix
  • valid-langmoderate Invalid language on part of the page WCAG 3.1.2 Level AA auto fix
Media Captions for video and no sound that starts on its own. 2 rules
  • video-captioncritical Video without captions WCAG 1.2.2 Level A
  • autoplay-mediaserious Media that plays sound on its own WCAG 1.4.2, 2.2.2 Level A auto fix
Motion and timing No blinking, no surprise refreshes, and motion that respects the setting. 3 rules
  • blink-marqueeserious Blinking or scrolling text WCAG 2.2.2 Level A
  • motion-reducedmoderate Large animation ignores reduced motion WCAG 2.3.3, 2.2.2 Level A
  • meta-refreshcritical Page refreshes or redirects on a timer WCAG 2.2.1 Level A
Tables Header cells that tie every data cell to its meaning. 3 rules
  • table-headersserious Data table without header cells WCAG 1.3.1 Level A
  • th-scope-validmoderate Invalid scope on a header cell WCAG 1.3.1 Level A auto fix
  • empty-table-headerminor Empty table header cell WCAG 1.3.1 Level A
ARIA Valid roles, attributes and values, and no hidden focusable content. 9 rules
  • aria-valid-roleserious Invalid ARIA role WCAG 4.1.2 Level A auto fix
  • aria-valid-attrserious Misspelled or unknown ARIA attribute WCAG 4.1.2 Level A auto fix
  • aria-valid-attr-valueserious Invalid ARIA attribute value WCAG 4.1.2 Level A
  • aria-required-attrcritical ARIA role missing a required state WCAG 4.1.2 Level A
  • aria-hidden-focusserious Hidden content that can still receive focus WCAG 4.1.2, 1.3.1 Level A
  • aria-hidden-bodycritical Whole page hidden from screen readers WCAG 4.1.2 Level A auto fix
  • aria-prohibited-labelmoderate aria-label on an element that cannot be named WCAG 4.1.2 Level A
  • aria-required-parentcritical ARIA role outside of its required container WCAG 1.3.1 Level A
  • presentation-role-conflictminor role="presentation" that browsers ignore WCAG 4.1.2 Level A auto fix
  • Critical Treat now
  • Serious Treat today
  • Moderate Book a visit
  • Minor Keep an eye on

API

A small API for scripts and tests

audit.ts
import { audit, fixHtml, suggestColors, formatText } from "a11ymedic";

const result = await audit('<img src="team.jpg">', { level: "AA", lang: "en" });
const [finding] = result.findings;
finding.rule;         // "image-alt"
finding.severity;     // "critical"
finding.message.pt;   // plain language, English and Portuguese
finding.wcag[0].url;  // https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html
finding.fix.after;    // '<img src="team.jpg" alt="Team">'

console.log(formatText(result));

const { output, applied } = await fixHtml(html);   // safe fixes only
suggestColors("#999999", "#ffffff").foreground;     // "#767676"
Main functions
FunctionWhat it does
audit(html | document | element, options?)Runs the rules. Returns findings (most severe first), human checks and counts.
auditDocument(root, options?)The same, synchronous, for a live DOM.
fixHtml(html, options?)Applies safe fixes to source text and keeps formatting. Idempotent.
applyFix(finding)Applies one fix to the live DOM.
contrastRatio(fg, bg), suggestColors(fg, bg, { ratio })WCAG contrast and the closest passing pair in OKLCH.
accessibleName(el), getRole(el)Accessible name and role, as assistive technology sees them.
formatText, toJSON, toMarkdown, toHtml, githubAnnotations, githubSummaryReports.
overlay(options?), drawPins(doc, findings)Browser build only: the pins and panel.

Full reference with every option: README on GitHub.

The reading room

Accessibility, explained by the medic

Short guides with the code to copy. Pick a pamphlet from the rack.

FAQ

Questions people ask the medic

What is the best free website accessibility checker?

Use one that finds problems and tells you how to fix them. a11ymedic is free and open source: it checks 61 WCAG 2.2 rules in the browser, on the command line and in CI, explains each problem in plain English or Portuguese and gives the patched HTML for every finding.

How do I check a website for accessibility problems?

Run npx a11ymedic https://your-site.com for a full report, or add the a11ymedic script tag (or the bookmarklet) to see numbered pins on the page itself. For your own code, run npx a11ymedic "src/**/*.html" and add --fail-on serious in CI.

Can a11ymedic fix accessibility issues automatically?

It fixes the safe ones: a11ymedic fix "src/**/*.html" shows a diff and --write applies it. Safe fixes are things like an empty alt on a spacer image, tabindex 0 instead of 5, re-enabling zoom or a misspelled ARIA attribute. Everything that needs a human decision (alt text that describes a photo, a label text) comes as a ready to paste suggestion instead.

Does a11ymedic replace testing with real users?

No. Automated checks catch a large share of common problems, but not all of them: whether alt text is accurate, whether a flow makes sense with a screen reader or whether captions are correct still need people. a11ymedic marks what it cannot decide as a human check.

Does it work with React, Vue, Angular and Next.js?

Yes. Test rendered components with expect(await audit(container)).toHaveNoViolations() in Vitest or Jest, check built HTML with the CLI, or load the overlay script in development.

Is a11ymedic an accessibility overlay?

No. Its overlay is a diagnosis tool for the people who build the site: it highlights problems and shows the fix. It never changes the page for visitors. Real fixes belong in your source code.