Overview
Accessibility testing that tells you what it can't tell you.
Deterministic accessibility testing for Playwright, Cypress, Puppeteer, Selenium, WebdriverIO, Jest/Vitest, and CI pipelines.
@surea11y/core is an accessibility testing engine you run in your own test suite, CI pipeline, or from the command line.
What sets it apart is what it does with the cases automated testing can't settle. It implements the W3C's open Accessibility Conformance Testing (ACT) Rules Format, verified against ACT's own published test corpus rather than judged only against itself. It reports findings, non-findings, and — unusually — explicit uncertainty, so results are auditable rather than reassuring.
Sure means certainty about what is known, and honesty about what isn't. It tells you what it can't tell you.
See what changed in the latest release
Not a single product
surea11y is a family of packages sharing one engine, not one tool with one install command. Install the one that matches how you already test, together with @surea11y/core, the engine it runs. Install core on its own to run the engine on a DOM you already have, or to build a tool of your own on it:
| I want to… | Install |
|---|---|
| Add accessibility checks to Playwright tests | @surea11y/playwright + @surea11y/core |
| …Puppeteer | @surea11y/puppeteer + @surea11y/core |
| …Selenium | @surea11y/selenium + @surea11y/core |
| …Cypress | @surea11y/cypress + @surea11y/core |
| …WebdriverIO | @surea11y/webdriverio + @surea11y/core |
| Assert accessibility in Jest/Vitest component or unit tests | @surea11y/test-matchers + @surea11y/core |
| Scan pages from a terminal or CI pipeline, as static HTML or in Chromium | @surea11y/cli |
| Check against EN 301 549, the European standard, with any of the above | @surea11y/pack-en301549 + @surea11y/core |
| Check against your own rules, profiles or standard, public or internal | A pack of your own: see Writing a pack |
| Run the engine against a DOM you already have | @surea11y/core |
| Build your own tool on the engine: a CLI, an extension, a dashboard, a report generator | @surea11y/core |
See Getting Started for install and usage per binding. If you're building a tool, BUILDING_ON_CORE.md on GitHub covers the stable contract, running scans, choosing checks, and presenting results without claiming more than they say.
For a first look without a test suite, load the standalone bundle on a page and scan it from the browser console: await a11ycore.run() scans the page, a11ycore.table(result) lists the findings, a11ycore.highlight(result) outlines them on the page, and await a11ycore.openReport(result) opens the HTML report. INTEGRATION.md on GitHub lists the other helpers, such as a11ycore.inspect('headings') for a page's headings, landmarks, names or focus order.
How its results differ
Every rule makes one deterministic decision, from a fixed vocabulary of four outcomes — not the violations/passes/incomplete/inapplicable shape other popular accessibility testing tools use:
- fail (
fail) — a violation provable from the DOM. Reserved for objective, normative cases. - pass (
pass) — this rule's specific condition is met. Not a claim that the page is accessible. - can't tell (
cantTell) — a human has to decide this, and the result says what was ambiguous. - n/a (
notApplicable) — the rule's precondition isn't present.
cantTell is the point of the project. An engine that quietly discards what it cannot determine produces a shorter report and a false sense of coverage. See Results & Reports for the full result shape, and Conformance for how individual results roll up into a WCAG verdict — including how a target WCAG version (2.0/2.1/2.2) is resolved and applied.
A cantTell doesn't leave you with only a question. Every one a core rule reports says how to answer it: a few steps starting from the element it names, and what you'll see if it passes and if it fails, in the scan's language. Where the page gives evidence, the finding also leans toward pass or fail, with the facts it read, its reason, and what could make it wrong. Each lean is counted against the W3C ACT test cases, and once it has been counted on at least five, it says how often it was right; until then it carries no figure at all, never an estimate. The outcome stays cantTell: a lean orders the questions, and a person still answers them. See How to check and Leans in the Output schema, and LEANS.md on GitHub for what earns a finding a lean and how far to trust one.
The HTML report sets out the questions in the order a reviewer works through them: "Likely fails: confirm", "Likely passes: confirm" and "Needs a person". Each card has its "How to check" steps, and those in the first two groups say why they lean. In CI, the JUnit and SARIF reports can also gate on the findings that lean fail, off by default, and a baseline can accept the ones a page already has, so only new ones break the build. See HTML report and Baseline / allowlist, or open a sample report of a small checkout page.
A pass that came close says so. Rules that measure against a threshold — contrast, target size, text spacing — report a margin when the closest pass came near the limit: the element that came closest while still meeting it. Text at 4.52:1 against a 4.5:1 minimum passes, and the margin shows it, so you can fix it before a small change turns it into a fail. See the Output schema.
Name the standard, and the engine picks the rules. { wcag: { version: '2.2', level: 'AA' } } runs the rules for WCAG 2.2's A and AA criteria, by the level each criterion has in that version, and bestPractices: true adds the checks WCAG doesn't require. A standard beyond WCAG, such as a national one or your organisation's own policy, comes as a pack: its rules, profiles and checklist items run beside core's and show in every report. See Engine options and Packs.
Name checks rather than a standard, and the result still says which WCAG criteria they bear on. A scan that runs only link-name-present reports 2.4.4 and 4.1.2 too, plus the AAA 2.4.9 unless the scan sets a level, and a criterion whose other checks the scan left out comes back cantTell ("Partly checked" in the HTML report), not pass. wcagLevel: 'AA' sets the level of the criteria a scan reports, and the JUnit, SARIF and EARL reports follow it, so an AA build doesn't fail on an AAA criterion.
No WCAG criterion is reported as passed on the strength of rules that check only part of it. A criterion passes only when its rules check everything it asks, and none is checked that completely yet, so where every rule for a criterion passes, the criterion comes back cantTell: the parts the rules check are met, and a person checks the rest. A report with no failure says the same under its headline: no rule failed, and that is not a claim that the page conforms. Marking those criteria as passed would be the false sense of coverage this engine is built to avoid. See WCAG conformance mapping.
Web components are judged by what they expose. A custom element that gives itself a name, states or a role through ElementInternals is checked against them where the engine can read them, and asked about where it can't. For test and audit runs, internalsHookScript() is an opt-in script that lets a scan read the internals a component keeps private, so those elements pass or fail instead of coming back cantTell. INTEGRATION.md on GitHub shows how to install it before the page's own scripts.
Rules reach three criteria no rule covered before: 3.3.1 Error Identification (a field marked invalid whose error message is missing, hidden or not tied to it), 4.1.3 Status Messages (a live region assistive technology can't reach) and 1.4.5 Images of Text (text drawn in an image that could be real text). All three ask rather than fail. The keyboard rules read event handlers from the markup; with listenerHookScript(), another opt-in script installed before the page's own, they judge listeners added from script too, so a role="button" in the tab order with a click listener and no key listener anywhere its keys could reach one (on it, inside it, on its ancestors, a shadow root it is in, the document or the window), and no focus listener that might add one, fails: Enter and Space do nothing. INTEGRATION.md on GitHub shows how to install it, and its counterpart for status messages, the live-region hook.
In a browser, contrast is measured as the page is drawn: text on a page with no background of its own, on the canvas the browser paints, light or dark; generated text from ::before, ::after and list markers; a closed <select>. These used to be skipped or asked about. Text drawn only by an outline or a shadow now fails where every color it is drawn in is below the ratio. A lone character standing in for a control's name, the "X" in a button named "Close", is asked about rather than failed: whether it's an icon or a letter takes a person.
Checked against the ACT corpus
The engine ships 145 rules. Those with a W3C ACT Rules counterpart run against the test cases W3C publishes for them, the set W3C's own implementation listing scores: 957 examples across 60 ACT rules. The engine fails none of the examples ACT marks passed or inapplicable, so it reports no false positives against that corpus. That is a result on those examples, not a guarantee for every page; where the engine can't decide a case, it returns cantTell, which ACT permits for an automated implementation.
It holds in both places the engine runs: in jsdom, as test-matchers and the CLI run it by default, and in Chromium, as the browser bindings do, with each test case loaded as a real page, its scripts and linked style sheets included. Twenty-two examples ACT marks failed go unflagged in jsdom and 18 in Chromium. Most are judgement calls, such as whether a heading describes the content under it; the four a browser catches need a page's own scripts or style sheets, which jsdom doesn't run. See ACT_RULE_MAPPING.md for every one, with the reasoning.
Two EARL implementation reports record the outcome for every one of those cases, passes and inapplicable results included, so a rule that stayed silent because nothing applied is distinguishable from one that isn't implemented at all: SureA11y, run in jsdom, and SureA11y in a browser, run in Chromium. See EARL report for the format.
Key principles
- Deterministic execution. The same page, rendered under the same conditions and scanned with the same options, gets the same decisions. Timings, timestamps and environment details in the result can still differ between runs. Running animations and transitions are held still for the scan, so a fade in progress doesn't change what the rules measure.
- Conservative by design. Core's rules reserve
failfor objective, normative violations, to keep false alarms rare. - Standards traceability. Rules map to the applicable WCAG Success Criterion whenever appropriate.
- Stable machine-readable output. Rule identifiers remain stable regardless of language.
- Framework independent. Run inside jsdom or any browser automation framework.
- Extensible. Add custom rules, register policies and filter scans by rule IDs, tags or WCAG version.
- Localized reporting. Human-readable messages can be translated without affecting machine-readable data — ships with
en,fr,de,es, andjatoday.
Philosophy
Automate what can be determined objectively. Never pretend to automate what cannot.
Some WCAG requirements can be checked with complete confidence. Others need human judgement, knowledge of context, or usability evaluation. A single score or a pass/fail verdict flattens that difference; surea11y reports it. This is why cantTell and notApplicable exist as outcomes, and it shapes every rule in the engine.
What surea11y won't catch
Being explicit about the boundaries of automation is part of the same philosophy. For example, surea11y will not:
- confirm that alt text is meaningful, only that it is present (an alt attribute of
"image123.png"passes the objective check, and is asked about, leaning fail, as a file name); - judge whether a color contrast choice is aesthetically appropriate, only whether it meets the applicable contrast ratio;
- determine whether an error message actually explains the problem, since that depends on validation logic a static scan can't see;
- detect a keyboard focus trap or content clipped at 400% zoom, since both require simulating real user interaction over time, not just reading the DOM at one instant.
Some implemented checks report cantTell when a person has to decide. Other aspects, including keyboard traps and reflow at zoom, are not checked by the engine at all and need separate testing: no cantTell stands in for them, so a result without one doesn't mean they were covered. See Known limitations for the complete list.
About the project
surea11y is an independent open-source project, developed in the open.
@surea11y/core and every integration package are published on npm. Issues, discussion, and changes all happen publicly on GitHub, and contributions are welcome. See CONTRIBUTING.md for contribution guidelines.