Engine options
Every runner (runDomRulesInPage, runa11yCoreInPage) takes the same four arguments: (pageUrl, contextSelector, engineOptions, runOnly). This page documents engineOptions and runOnly in full.
Selecting which rules run
There are two independent ways to select rules — the 4th argument (runOnly), or engineOptions.rules/.tags/.tests/.includeMode. If runOnly contains any filter, it wins outright; otherwise the engine falls back to engineOptions. Don't mix them expecting both to apply — pick one.
Via runOnly (4th argument)
runDomRulesInPage(url, null, {}, {includeRuleIds: ['img-alt-present', 'button-name-present'],excludeRuleIds: ['region'],tags: ['wcag412'],excludeTags: ['best-practice'],includeMode: 'and' // 'and' (default) | 'or' — see below});
runOnly must be this object shape, not a bare array. runOnly: ['img-alt-present'] (a plain array) is silently ignored; the engine runs every rule instead. This is the single most common integration mistake — see the FAQ.
| Field | Type | Meaning |
|---|---|---|
Each of these accepts either an array or a comma-separated string, matching the engineOptions form below — includeRuleIds: 'img-alt-present, button-name-present' and includeRuleIds: ['img-alt-present', 'button-name-present'] are equivalent.
Rule IDs are bare (no engine prefix), e.g. 'img-alt-present'. For backward compatibility, matching also accepts a legacy a11ycore--prefixed form of the same id ('a11ycore-img-alt-present').
A legacy tag-filter shape is also accepted as the whole runOnly value: { type: 'tag', values: ['wcag2a', 'wcag2aa'] } — equivalent to { tags: ['wcag2a', 'wcag2aa'] }.
Filtering by WCAG version (2.0, 2.1, 2.2)
Every rule and composite carries exactly one WCAG-version-origin level tag: wcag2a/wcag2aa/wcag2aaa for a Success Criterion that's WCAG 2.0 baseline, wcag21a/wcag21aa/wcag21aaa for one newly introduced in WCAG 2.1 (e.g. 1.3.5 Identify Input Purpose), wcag22a/wcag22aa/wcag22aaa for one newly introduced in WCAG 2.2 (e.g. 2.5.8 Target Size Minimum). A rule gets only the tag for its SC's actual origin version — a 2.1-introduced SC is never also tagged wcag2aa, since it doesn't exist under a WCAG 2.0 conformance target.
Since versions are cumulative (2.1 = 2.0 + new; 2.2 = 2.0 + 2.1 + new), select a WCAG-version conformance target by combining tag sets — the engine's OR-matching on tags (any one match includes the rule) does the rest:
// WCAG 2.0 AA only (excludes every 2.1/2.2-introduced SC, even at level AA):{ tags: ['wcag2a', 'wcag2aa'] }// WCAG 2.1 AA conformance (2.0 baseline + everything 2.1 added, both at A and AA):{ tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] }// WCAG 2.2 AA conformance (2.0 baseline + 2.1 additions + 2.2 additions):{ tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'] }// Just the SCs 2.2 introduced, nothing else:{ tags: ['wcag22a', 'wcag22aa', 'wcag22aaa'] }
One SC goes the other way. WCAG 2.2 removed SC 4.1.1 Parsing — the only criterion ever dropped rather than added. A rule mapped to it carries its 2.0-origin tag (wcag2a) like any other baseline rule, plus wcag22-removed, and the version tag sets above therefore include it under a 2.2 target, where it does not belong.
You do not have to do anything about that. The engine resolves a target WCAG version for every run and, when that target is 2.2, a wcag22-removed rule cannot report fail: it still runs, still reports every occurrence it found, but its outcome is coerced to cantTell and the result carries a wcagVersionScope field saying why (see the output schema). Nothing is silently dropped, and a 2.2 run is not gated by a criterion 2.2 does not contain.
The target version is resolved in this order:
engineOptions.wcagVersion—'2.0','2.1'or'2.2', if you set it.- The version-origin tags in your own filter: a set topping out at
wcag21a/wcag21aareads as a 2.1 target, one containing anywcag22*tag as 2.2, one with onlywcag2*tags as 2.0. Only those nine tags count — an SC tag (wcag411) orbest-practicesays nothing about a version. - Otherwise
'2.2', this engine's default target.
// Nothing to declare: a plain run already targets 2.2, so a duplicate id// comes back cantTell rather than fail.runDomRulesInPage(url, null, {}, null);// Conformance-testing against 2.1, where SC 4.1.1 still exists:runDomRulesInPage(url, null, { wcagVersion: '2.1' }, null);// Same thing, implied by the tag set — no extra option needed:runDomRulesInPage(url, null, {}, { tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] });
The resolved target is reported back on every result as engine.wcagVersion, so you can confirm which one a run actually used.
If you would rather not see the rule at all under 2.2, exclude it outright — the tag is still there for exactly that:
// WCAG 2.2 AA conformance, with the removed criterion left out entirely:{tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'],excludeTags: ['wcag22-removed']}
duplicate-id is the only rule carrying that tag today. Left in, it still reports something real — a duplicate id breaks <label for>, fragment links and getElementById whatever the standard says — it just is not a 2.2 conformance failure.
Via engineOptions (no runOnly)
Same filtering, expressed as comma-separated strings (or arrays) nested in engineOptions:
runDomRulesInPage(url, null, {rules: { include: 'img-alt-present, button-name-present', exclude: 'region' },tags: { include: 'wcag412', exclude: 'best-practice' },includeMode: 'and'}, null);
rules.include/.exclude, tags.include/.exclude, tests.include/.exclude (alias of rules), and top-level includeMode mirror the runOnly fields above exactly. Comma-separated strings are trimmed, de-duplicated, and empty tokens dropped automatically.
engineOptions — the rest
const engineOptions = {locale: 'en', // default 'en'; de-DE falls back to de, then to en per stringwcagVersion: '2.2', // default '2.2' — the conformance targetmessages: { de: { /* key: text */ } }, // optional caller-supplied dictionaries; win over built-in onesincludeHiddenElements: false, // default false — set true to evaluate hidden/collapsed subtrees tooincludeShadowDom: true, // default true — opt OUT with false to skip open shadow rootsfragment: false, // default false — set true when the scan target isn't a real pageexcludeSelectors: ['#cookie-banner', '.third-party-widget'], // array or comma-separated stringtimestamp: '2026-07-20T12:00:00Z', // optional — engine has no built-in clockperfStats: false, // default false — internal timing counters, debug-only shapeprofileRules: false, // default false — per-rule timings; needs perfStatscontrast: {mode: 'strictConformance', // 'strictConformance' (default) | 'auditorAssist'rootCanvasFallback: '#ffffff' // background assumed when the true root background isn't computable},visibilityMode: 'styleOnly', // 'styleOnly' (default) | 'styleAndGeometry' — scoped to the contrast rules onlypolicyContract: 'a11y', // 'a11y' (default) | 'generic' | inline contract objectpolicy: { // optional overrides on top of policyContractcoerceManualFailToCantTell: true},output: {includeSelector: true, // set false to suppress auto-filled selectorsincludeHtml: true},rules: {'some-rule-id': {excludeSelectors: ['.some-noisy-widget'] // narrows candidates for THIS rule only}},probes: { /* optional host-supplied evidence */ },customRules: [ /* runtime-registered rules, see below */ ],// Only read by runa11yCoreAcrossFrames. Ignored by runDomRulesInPage/runa11yCoreInPage.pingWaitTime: 500, // ms to wait for a child frame to answer a pingframeWaitTime: 60000 // ms to wait for a child frame's full scan result};
| Option | Meaning |
|---|---|
Rule-scoped excludeSelectors
The top-level excludeSelectors applies to every rule — there's no way to exclude an element from just one rule while still running every other rule against it. rules[ruleId].excludeSelectors fills that gap: it narrows candidates for that one rule only, on top of (never instead of) the global list.
const engineOptions = {excludeSelectors: ['#cookie-banner'], // applies to every rule, as alwaysrules: {'aria-required-children': {excludeSelectors: ['mat-select', 'mat-stepper', 'mat-horizontal-stepper', 'mat-vertical-stepper']},'aria-allowed-attr': {excludeSelectors: ['mat-progress-spinner']}}};
Why you'd want this: Angular Material's <mat-select> builds its internal ARIA structure in a way that trips a false positive on aria-required-children specifically, even though the component is otherwise fine. With only the global excludeSelectors, the only way to silence that false positive is excludeSelectors: ['mat-select'] — which also hides mat-select from every other rule, including contrast-minimum and aria-allowed-attr, silently dropping real coverage those checks never had a problem with. The example above keeps mat-select fully visible to every rule except the one that misfires on it.
Effective exclusions for a given rule are the union of the global list and that rule's own list — an element matching either is dropped from that rule's candidates. A rule whose only would-be-failing elements are all excluded this way reports outcome: 'pass' or 'notApplicable' (matching that rule's own no-candidates convention), with occurrences: [] — never outcome: 'fail' with an empty occurrences array, since that exact shape is reserved elsewhere in the schema to mean "this rule threw" (see the output schema).
Accepts the same forms as the global option: an array (['mat-select', 'mat-stepper']) or a comma-separated string ('mat-select, mat-stepper').
If you're using a binding package (@surea11y/binding-base and its Playwright/Puppeteer wrappers), check that binding's own README for whether its .exclude() builder method has a rule-scoped form yet — this is an engineOptions shape documented here at the engine level; not every binding has picked it up.
Recipes — composing options for real scenarios
CI gate: WCAG 2.2 AA only, ignore a third-party widget you don't control
runDomRulesInPage(url, null, {excludeSelectors: ['#cookie-banner', '.intercom-launcher'],tags: { include: 'wcag2a,wcag2aa,wcag21a,wcag21aa,wcag22a,wcag22aa' }}, null);
Human auditor doing a deep contrast pass in a real browser — trade some false-positive protection for more findings, and check real layout (not just computed style) since a real page is being driven. Shown with Puppeteer's page.evaluate (accepts multiple args); if you're on Playwright, wrap the four positional args into a single object first — see INTEGRATION.md:
const result = await page.evaluate(runa11yCoreInPage, url, null, {contrast: { mode: 'auditorAssist' },visibilityMode: 'styleAndGeometry'}, null);
Scoped re-scan of one region after a UI change, skipping shadow DOM — useful in a component-level test where you only care about the widget you just changed:
runDomRulesInPage(url, '#checkout-form', {includeShadowDom: false}, { includeRuleIds: ['form-control-programmatic-label-present', 'button-name-present'] });
Reproducible output for snapshot testing — pin a timestamp so two runs of the same HTML produce byte-identical JSON, and request the debug timing breakdown:
runDomRulesInPage(url, null, {timestamp: '2026-01-01T00:00:00Z',perfStats: true,profileRules: true}, null);
A custom, org-specific rule alongside the built-ins, only for this one call:
runDomRulesInPage(url, null, {customRules: [{id: 'org-no-inline-onclick',meta: { title: 'No inline onclick handlers', defaultSeverity: 'moderate' },runInPage(ctx) {const els = ctx.helpers.queryAll('[onclick]');const occurrences = els.map((el) => ({selector: ctx.helpers.buildSelector(el),html: el.outerHTML,summary: 'Inline onclick handler found.',hint: 'Move event handling into an external script.'}));return { ruleId: ctx.rule.ruleId, outcome: occurrences.length ? 'fail' : 'pass', severity: 'moderate', occurrences };}}]}, null);
customRules — runtime-registered rules
Every shipped rule is baked into src/core.js at build time. engineOptions.customRules is the runtime escape hatch: an array of rule descriptors registered for that one call only — nothing is added to the static catalog, and nothing persists between calls. This is deliberate, not a limitation to work around: surea11y already takes fresh engineOptions per call with no mutable global config (unlike some other engines, which need a configure()/reset() step against a shared runtime), and custom rules follow that same per-call model.
Calling the library directly is one way in; the CLI also exposes this via --custom-rules <path> (a local file, loaded once per scan) — see the CLI docs.
A descriptor has the same shape as an internal rule module's own export — if you already know how to write a rule file for this engine, you already know this API:
{id: 'my-org-custom-rule', // requiredmeta: { title, description, tags, defaultSeverity, defaultConfidence, /* same fields as a rule module's meta */ },runInPage(ctx) { /* same ctx shape and same return contract as any built-in rule */ },applicability(ctx) { return true; }, // optional, same contract as a built-in rule's applicabilitydata: { /* optional, JSON-serializable */ }}
runInPage/applicabilitymay be a real function or a function-source string (i.e.fn.toString()). Pass a real function whenengineOptionsnever leaves the current JS realm (plain Node/jsdom use). Pass a string when it does — e.g. a Playwrightpage.evaluate(runa11yCoreInPage, { engineOptions })call, whereengineOptionscrosses a JSON/structured-clone boundary that cannot carry a liveFunctionreference but can carry a string. The engine reconstructs a string vianew Function, the same mechanism the build uses to embed every built-in rule's source into the in-page runner.metagets identical defaulting/validation to a build-time rule — omit anything you don't need;severitydefaults tomoderate,confidencetomedium,typetoautomatic, etc.- A custom rule whose
idcollides with a built-in one overrides it for that scan, rather than running both. Since a same-named custom rule is just as likely to be an accidental collision as a deliberate override, every collision is surfaced two ways: aconsole.warnnaming the id(s), and a top-leveloverriddenBuiltinIdsarray on the result (empty when there's no collision) — see the output schema. - An invalid descriptor (missing/non-string
id, or arunInPagethat isn't a function and isn't a reconstructable source string) is silently skipped — the rest of the scan, including every built-in rule, still runs normally. This isn't a validation gap to fix: a custom rule is arbitrary caller-supplied code, so "fail this one entry closed, don't abort the scan" is the safer default, mirroring how a built-in rule that throws is contained to acantTellfor that rule rather than crashing the run. - Results appear in
checksResultsexactly like any other rule's, including automaticselector/html/structuralPathfill-in forfail/cantTelloccurrences that only attach{ __node }(see the output schema).
contextSelector (2nd runner argument, not an engineOptions field)
A CSS selector (or array of selectors) scoping the scan to one or more subtrees, resolved via document.querySelectorAll (all matches, not just the first), falling back to document.documentElement/document.body if nothing matches. Pass null to scan the whole document.
- A single string may itself be a comma-separated selector list (ordinary CSS union semantics) —
'#a, #b'scans both#aand#b. - An array of strings scans the union of every selector's matches —
['#a', '.card']behaves the same as'#a, .card'; the array form exists for callers building the list programmatically. - Overlapping/nested regions are deduped automatically — an element reachable from more than one matched root is only ever reported once, not once per region.