Skip to content

WCAG Validation Rules β€” Complete Reference

This document describes every accessibility rule checked by all three validation layers in the Accessible PDF Converter pipeline.

Last updated: 2026-03-07 (updated to reflect 7 new Tier 1 auto-fixes; AAA rules section added)


Architecture Overview

The pipeline runs three complementary validation layers on every converted HTML file:

LayerEngineSpeedRulesAuto-FixRuns When
Custom Static ValidatorRegex on HTML strings< 50ms38 rules20 auto-fixableBefore saving HTML β€” fixes issues upstream
Browser Custom CheckersPuppeteer + JS evaluation1–2s each3 checksNoAfter conversion β€” checks layout/zoom/spacing/focus
axe-core AuditorBrowser-based (Puppeteer)3–8s~90+ rules20 auto-fixableAfter saving β€” comprehensive audit with fix loop

The custom validator runs first as a fast pre-pass. It fixes common structural issues so that axe-core sees cleaner HTML and reports fewer violations. The browser custom checkers test criteria that require a rendered DOM but are not covered by axe-core. axe-core then performs a thorough audit using a real DOM, computed styles, and layout information.

Source files:

  • Custom validator: workers/api/src/services/wcag-validator.ts
  • Color utilities: workers/api/src/utils/color.ts
  • Resize checker: workers/api/src/services/wcag-resize-checker.ts
  • Text spacing checker: workers/api/src/services/wcag-text-spacing-checker.ts
  • Focus order checker: workers/api/src/services/wcag-focus-order-checker.ts
  • axe-core auditor: workers/api/src/services/axe-validator.ts
  • axe-core fixer: workers/api/src/services/axe-fixer.ts

Part 1: Custom Static Validator Rules (38 rules)

The custom validator operates entirely on HTML strings using regex pattern matching and DOM walking via node-html-parser. It has no rendered layout, no computed styles, and no JavaScript execution. Its value is speed and upstream fixing β€” issues are corrected before the HTML is persisted.

Document Structure Rules

#Rule IDWCAG SCLevelAuto-FixDescription
1document-title2.4.2AYesDocument must have a <title> element
2html-has-lang3.1.1AYes<html> element must have a lang attribute
3document-lang-valid3.1.1ANolang attribute value must be a valid BCP 47 code (e.g., en, fr-CA)
4meta-viewport1.4.4AANoViewport meta tag must be present
5landmark-one-mainBest PracticeAYesDocument should have one <main> landmark
6skip-link2.4.1AYesDocument should have a skip-to-content link

Auto-fix details:

  • document-title β€” Inserts <title>Converted Document</title> into <head>.
  • html-has-lang β€” Adds lang="en" to the <html> element.
  • landmark-one-main β€” Wraps body content in <main role="main">.
  • skip-link β€” Inserts <a href="#main-content" class="skip-link">Skip to main content</a> after <body>.

Text and Naming Rules

#Rule IDWCAG SCLevelAuto-FixDescription
7image-alt1.1.1AYes<img> elements must have an alt attribute
8image-alt-meaningful1.1.1ANoalt attribute must not be a generic or filename-based value (alt="image", alt="photo", alt="img_001.png")
9link-name2.4.4AYesLinks with href must have discernible text or aria-label
10button-name4.1.2AYesButtons must have discernible text or aria-label
11label1.3.1AYesForm inputs (except hidden/submit/button/reset/image) must have associated labels
12empty-heading1.3.1AYes<h1> through <h6> elements must not be empty

Auto-fix details:

  • image-alt β€” Adds alt="Image - description needed" to images missing alt.
  • link-name β€” Adds aria-label="Link - description needed" to empty links.
  • button-name β€” Adds aria-label="Button - description needed" to empty buttons.
  • label β€” Adds aria-label="Input field - label needed" to unlabeled inputs.
  • empty-heading β€” Removes the empty heading element entirely.

Heading and Landmark Rules

#Rule IDWCAG SCLevelAuto-FixDescription
13heading-order1.3.1AAYesHeading levels should increase sequentially (no jumps from h1 to h3)
14heading-descriptive2.4.6AANo βš‘Heading text must be descriptive; generic values (β€œSection 2”, β€œContinued”, purely numeric) are flagged as warnings

Table Rules

#Rule IDWCAG SCLevelAuto-FixDescription
15table-has-header1.3.1AYes<table> elements must contain at least one <th>
16empty-table-header1.3.1ANo βš‘<th> elements must not be empty
17scope-attr-valid1.3.1AYesscope attribute must be row, col, rowgroup, or colgroup
18th-has-scope1.3.1ANo βš‘Every <th> must have a scope attribute
19layout-table1.3.2AYesTables with no <th>, no <caption>, and cells containing only block elements are flagged as probable layout tables and tagged role="presentation" (warning)

Auto-fix details:

  • table-has-header β€” Promotes <td> cells in the first <tr> to <th scope="col">.
  • scope-attr-valid β€” Removes invalid scope attributes.
  • layout-table β€” Adds role="presentation" to tables identified as layout-only. Does not restructure the table β€” CSS layout conversion must be done manually for full compliance.

List Rules

#Rule IDWCAG SCLevelAuto-FixDescription
20list-structure1.3.1AYes<ul> and <ol> must only contain <li> as direct children; orphan <li> outside lists is flagged
21definition-list1.3.1AYes<dl> must only contain <dt>, <dd>, or <div> as direct children

Auto-fix details:

  • list-structure β€” (1) Non-<li> direct children of <ul>/<ol> are wrapped in <li> using DOM parsing. (2) Orphan <li> elements outside any list are wrapped in <ul>. Nested lists are preserved.
  • definition-list β€” Invalid direct children of <dl> (anything other than <dt>, <dd>, <div>) are wrapped in <dd> using DOM parsing.

ARIA and Parsing Rules

#Rule IDWCAG SCLevelAuto-FixDescription
22duplicate-id4.1.1AYesElement id attributes must be unique within the document
23invalid-nesting4.1.1ANoBlock elements (<div>, <p>, <ul>) inside inline elements (<span>, <a>) is invalid HTML; anchor-inside-anchor and interactive-inside-interactive are also flagged
24aria-role-valid4.1.2AYesrole attribute values must be from the ARIA specification (e.g., role="dropdown" is invalid)
25aria-allowed-attr4.1.2ANo βš‘aria-* state/property attributes must be compatible with the element’s role (e.g., aria-checked on a plain <div>)
26status-messages4.1.3AAYesDocuments with forms or feedback containers should include a live region (role="alert", role="status", or aria-live) for dynamic status announcements

Auto-fix details:

  • duplicate-id β€” Appends -2, -3, etc. suffix to second and subsequent occurrences of the same id. Also updates any aria-labelledby, aria-describedby, aria-controls, aria-owns, aria-flowto, aria-activedescendant, and for attributes that referenced the renamed ID.
  • aria-role-valid β€” Removes the invalid role attribute entirely. The element falls back to its implicit ARIA role, which is always preferable to an invalid one.
  • status-messages β€” If feedback-pattern elements exist (class/id matching alert|error|notice|status|notification|message|feedback), adds aria-live="polite" role="status" aria-atomic="true" to them. Otherwise injects a visually-hidden <div role="status" aria-live="polite"> after <body> as a general announcer.

Color and Sensory Rules

#Rule IDWCAG SCLevelAuto-FixDescription
27color-contrast1.4.3AAYes (inline only)Inline text styles must have sufficient contrast ratio against their background (real WCAG luminance calculation). Color failures caused by CSS cascade, var(), or external stylesheets are not fixable statically.
28use-of-color1.4.1ANo βš‘Inline color styles on short text elements without a semantic indicator or icon are flagged as possible color-only information (warning)
29sensory-characteristics1.3.3ANo βš‘Instructions referencing shape, color, size, or location only (β€œclick the green button”, β€œsee the box on the right”) are flagged as warnings
30images-of-text1.4.5AANo βš‘<img> elements with long prose alt text (>8 words) and no logo/decorative indicator are flagged as possible images of text (warning)

Auto-fix detail β€” color-contrast (inline styles):

For inline style="color: X; background-color: Y" violations, the fix computes which of #1a1a1a (dark) or #ffffff (white) achieves higher contrast against the known background and replaces the color property value. This handles author-specified inline palette violations. It does not fix contrast failures caused by CSS cascade, CSS variables (var()), inherit, currentColor, or external stylesheets β€” those require axe-core’s computed-style check and CSS-level fixes.

Color contrast known injected color pairs:

ElementForegroundBackgroundRatioAA (4.5:1)AAA (7:1)
Body text#1a1a1a#ffffff17.4:1PassPass
Figcaption#555555#ffffff7.5:1PassPass
Secondary text#4a4a68#ffffff7.0:1PassPass
Heading text#1a1a2e#ffffff16.0:1PassPass
Source header#495057#f8f9fa7.0:1PassPass

Scope limitation: Does not resolve CSS cascade, var(), inherit, currentColor, or external stylesheets β€” those remain axe-core’s job.

Language Rules

#Rule IDWCAG SCLevelAuto-FixDescription
31lang-of-parts3.1.2AANo βš‘When the document is declared as a Latin-based language (e.g., lang="en") but contains CJK, Arabic, Cyrillic, Hebrew, Devanagari, Greek, or Thai characters outside of an element with a lang attribute, a warning is issued
#Rule IDWCAG SCLevelAuto-FixDescription
32consistent-identification3.2.4AANo βš‘Interactive elements (<button>, <a>, <input type="submit">) with the same visible label but different element types are flagged as inconsistent identification (warning)

AAA Rules (checked when AAA target level is selected)

#Rule IDWCAG SCLevelAuto-FixDescription
33color-contrast-enhanced1.4.6AAANo βš‘Text must achieve a 7:1 contrast ratio (3:1 for large text) β€” checked only against known injected color pairs
34visual-presentation1.4.8AAAYestext-align: justify and user-select: none are flagged and removed from all inline styles and <style> blocks
35images-of-text-no-exception1.4.9AAANo βš‘<img> elements whose alt text contains >20 characters of prose are flagged as likely images of text
36link-purpose-sole2.4.9AAANo βš‘Links with identical text that point to different destinations are flagged as ambiguous
37section-headings2.4.10AAANo βš‘Documents with more than one long text block and no headings are flagged for missing section headings
38abbreviation-expansion3.1.4AAANo βš‘Common abbreviations not wrapped in <abbr title="..."> are flagged as warnings

Auto-fix detail β€” visual-presentation:

  • Removes text-align: justify from all inline style="..." attributes and <style> blocks (justified text creates uneven word-spacing that impacts readability for users with dyslexia).
  • Removes user-select: none from all inline style="..." attributes and <style> blocks (prevents users from copying text, violating the user’s right to interact with content).

βš‘ Rules Flagged But Not Auto-Fixed

The following rules are detected and reported in the audit output but no automated fix is applied. They require content knowledge, human judgment, or structural changes that cannot be safely made without understanding the document.

These violations will appear in the final report. The user must resolve them manually.

Rule IDWCAG SCLevelWhy Not Fixed
document-lang-valid3.1.1ACannot determine the correct BCP 47 language code from context
image-alt-meaningful1.1.1ARequires understanding what the image depicts β€” needs AI vision or human review
empty-table-header1.3.1ACannot supply the missing header label without knowing what column it represents
th-has-scope (complex tables)1.3.1AStructural scope assignment in complex multi-row/col-span tables requires human review
aria-allowed-attr4.1.2AIncompatible ARIA attribute is reported as a warning β€” strip would be unsafe without knowing intended widget pattern
invalid-nesting4.1.1ABlock-in-inline and anchor-inside-anchor detected; restructuring may change document meaning
meta-viewport (zoom disabled)1.4.4AAPresence checked; if user-scalable=no is intentional, not auto-removed
heading-descriptive2.4.6AARewriting heading content requires understanding what section the heading introduces
use-of-color1.4.1AProse rewrite or icon addition required β€” cannot add semantic meaning automatically
sensory-characteristics1.3.3AProse rewrite required β€” cannot change instruction wording without content understanding
images-of-text1.4.5AACannot convert a rasterized image to actual text
lang-of-parts3.1.2AAScript detected but correct BCP 47 code cannot be inferred without a language-detection service
consistent-identification3.2.4AAMay be intentional β€” requires content review to determine if inconsistency is a defect
color-contrast (CSS cascade)1.4.3AAFailures caused by external CSS, var(), inherit, or currentColor cannot be resolved by static HTML analysis
color-contrast-enhanced1.4.6AAAInline contrast corrected to AA level only; achieving 7:1 may not be possible without design changes
images-of-text-no-exception1.4.9AAACannot convert image to text
link-purpose-sole2.4.9AAASame-text links with different destinations require content-level rewrites
section-headings2.4.10AAACannot add headings without knowing where section boundaries belong
abbreviation-expansion3.1.4AAACannot supply expansion text without a domain-specific dictionary

Part 2: Browser Custom Checkers (3 checks)

These checkers use Puppeteer to load the HTML in a real headless browser and evaluate criteria that require actual rendering, computed layout, or DOM interaction. They are not covered by axe-core.

Source files: wcag-resize-checker.ts, wcag-text-spacing-checker.ts, wcag-focus-order-checker.ts

Runs via: npm run test:browser (integration tests); also callable as a service function in the conversion pipeline.

1.4.4 Resize Text β€” checkResizeText()

WCAG SC: 1.4.4 (Level AA)

Applies 200% zoom (document.documentElement.style.zoom = '2') to the rendered page and measures:

  • Horizontal overflow β€” scrollWidth > innerWidth indicates content does not reflow and requires horizontal scrolling at high zoom.
  • Clipped content β€” elements with overflow: hidden whose scrollWidth > clientWidth + 2px after zoom, meaning text has been cut off.

Returns: { passed, hasHorizontalScroll, clippedElements[] }

Common failures in PDF-to-HTML output: Fixed-width tables wider than the viewport, absolute-positioned elements, PDF column layouts with pixel widths.


1.4.12 Text Spacing β€” checkTextSpacing()

WCAG SC: 1.4.12 (Level AA)

Injects the WCAG 1.4.12 minimum text spacing overrides and checks for clipping:

* {
line-height: 1.5 !important;
letter-spacing: 0.12em !important;
word-spacing: 0.16em !important;
padding-top: 0.25em !important;
}

After injection, any text-bearing element with overflow: hidden whose scrollHeight > clientHeight + 2px has had its content clipped by the spacing change β€” a WCAG 1.4.12 failure.

Returns: { passed, clippedElements[] } β€” each entry includes before.height and after.height for diagnosis.

Common failures in PDF-to-HTML output: Fixed-height containers from PDF layout reconstruction, table cells with overflow: hidden and pixel heights.


2.4.3 Focus Order β€” checkFocusOrder()

WCAG SC: 2.4.3 (Level A)

Computes the theoretical keyboard tab sequence from the DOM and compares it to DOM source order:

  1. Collects all focusable elements: a[href], button, input, select, textarea, [tabindex]
  2. Identifies missed elements: interactive elements with tabindex="-1" (explicitly removed from tab order β€” a problem if there is no alternative keyboard path)
  3. Builds the theoretical tab sequence: elements with positive tabindex values come first (ascending), then elements with tabindex="0" in DOM order
  4. Warns when an element’s tab position differs from its DOM position by more than 2 β€” indicating a positive tabindex is disrupting the natural reading order

Returns: { passed, warnings[], missedElements[] }

Threshold: Elements within Β±2 positions of their DOM position are not flagged (minor reordering is tolerated). Elements with explicit positive tabindex that jump significantly (e.g., tabindex="1" pulling a mid-page element to position 0) are flagged.

Note on axe-core overlap: axe-core’s tabindex best-practice rule flags any tabindex > 0 as a violation. This checker provides additional context by identifying how much the order is disrupted and which elements are removed from the tab sequence.


Part 3: axe-core Auditor Rules (~90+ rules)

axe-core v4.10.2 runs in a Puppeteer browser instance with full DOM, computed styles, and layout information. It is configured with the following tag filters:

wcag2a, wcag2aa, wcag2aaa, wcag21a, wcag21aa, wcag21aaa, wcag22a, wcag22aa, wcag22aaa, best-practice

This covers WCAG 2.0 through 2.2 at all levels (A, AA, AAA) plus best practices β€” approximately 90–100 unique rules.

WCAG 2.0/2.1/2.2 Level A Rules

Rule IDDescriptionWCAG SC
area-altImage map <area> elements must have alternate text1.1.1
aria-allowed-attrARIA attributes must be appropriate for the element’s role4.1.2
aria-braille-equivalentaria-braillelabel and aria-brailleroledescription must have non-braille equivalent4.1.2
aria-command-nameARIA buttons, links, and menuitems must have accessible names4.1.2
aria-conditional-attrARIA attributes must be used correctly for the current state4.1.2
aria-deprecated-roleDeprecated ARIA roles must not be used4.1.2
aria-hidden-bodyaria-hidden="true" must not be present on <body>4.1.2
aria-hidden-focusaria-hidden elements must not contain focusable elements4.1.2
aria-input-field-nameARIA input fields must have accessible names4.1.2
aria-meter-nameARIA meter elements must have accessible names1.1.1
aria-progressbar-nameARIA progressbar elements must have accessible names1.1.1
aria-prohibited-attrARIA attributes must not be prohibited for the element’s role4.1.2
aria-required-attrRequired ARIA attributes must be present4.1.2
aria-required-childrenARIA roles must contain required child roles4.1.2
aria-required-parentARIA roles must be contained by required parent roles4.1.2
aria-rolesARIA role values must be valid4.1.2
aria-toggle-field-nameARIA toggle fields must have accessible names4.1.2
aria-tooltip-nameARIA tooltip elements must have accessible names4.1.2
aria-valid-attr-valueARIA attribute values must be valid4.1.2
aria-valid-attrARIA attribute names must be valid4.1.2
blink<blink> elements must not be used2.2.2
button-nameButtons must have discernible text4.1.2
bypassPages must have a mechanism to bypass repeated content2.4.1
definition-list<dl> elements must be structured correctly1.3.1
dlitem<dt> and <dd> must be contained by a <dl>1.3.1
document-titleDocuments must have a <title> element2.4.2
duplicate-id-ariaIDs used in ARIA and labels must be unique4.1.1
form-field-multiple-labelsForm fields must not have multiple labels1.3.1
frame-focusable-contentFrames with focusable content must not have tabindex="-1"2.1.1
frame-title-unique<iframe> and <frame> elements must have unique titles4.1.2
frame-titleFrames must have accessible names4.1.2
html-has-lang<html> element must have a lang attribute3.1.1
html-lang-valid<html> element lang attribute must be valid3.1.1
html-xml-lang-mismatchlang and xml:lang must match3.1.1
image-altImages must have alternate text1.1.1
input-button-nameInput buttons must have discernible text4.1.2
input-image-alt<input type="image"> must have alternate text1.1.1
labelForm elements must have labels1.3.1
link-in-text-blockLinks must be distinguishable without relying on color1.4.1
link-nameLinks must have discernible text2.4.4
list<ul> and <ol> must only contain <li>, <script>, or <template>1.3.1
listitem<li> elements must be contained within <ul> or <ol>1.3.1
marquee<marquee> elements must not be used2.2.2
meta-refreshTimed meta refresh must not be used2.2.1
nested-interactiveInteractive controls must not be nested4.1.2
no-autoplay-audioAudio must not autoplay for more than 3 seconds1.4.2
object-alt<object> elements must have alternate text1.1.1
role-img-altElements with role="img" must have alternate text1.1.1
scrollable-region-focusableScrollable regions must be keyboard accessible2.1.1
select-name<select> elements must have accessible names1.3.1
server-side-image-mapServer-side image maps must not be used2.1.1
summary-name<summary> elements must have discernible text4.1.2
svg-img-altSVG elements with role="img" must have alternate text1.1.1
td-headers-attrheaders attribute values must refer to cells in the same table1.3.1
th-has-data-cellsTable headers must be associated with data cells1.3.1
video-caption<video> elements must have captions1.2.2

WCAG 2.0/2.1/2.2 Level AA Rules

Rule IDDescriptionWCAG SC
color-contrastText must have sufficient color contrast (4.5:1 normal, 3:1 large)1.4.3
valid-langlang attribute values must be valid3.1.2
meta-viewportViewport must not disable text scaling1.4.4
autocomplete-validautocomplete attribute values must be valid1.3.5
avoid-inline-spacingInline text spacing must be adjustable1.4.12
target-sizeTouch targets must be at least 24x24 CSS pixels2.5.8

WCAG 2.0/2.1/2.2 Level AAA Rules

Rule IDDescriptionWCAG SC
color-contrast-enhancedText must have enhanced color contrast (7:1 normal, 4.5:1 large)1.4.6
identical-links-same-purposeLinks with identical text must serve the same purpose2.4.9
meta-refresh-no-exceptionsTimed meta refresh must not be used (no exceptions)2.2.1

Best Practice Rules

Rule IDDescription
accesskeysaccesskey attribute values must be unique
aria-allowed-roleARIA roles must be appropriate for the element
aria-dialog-nameARIA dialog and alertdialog must have accessible names
aria-textrole="text" must be used correctly
aria-treeitem-nameARIA treeitem elements must have accessible names
empty-headingHeadings must not be empty
empty-table-headerTable header cells must not be empty
heading-orderHeading levels should increase sequentially
image-redundant-altImage alt text must not duplicate surrounding text
label-title-onlyForm elements should not use title as only label
landmark-banner-is-top-levelBanner landmark must be at top level
landmark-complementary-is-top-levelComplementary landmark must be at top level
landmark-contentinfo-is-top-levelContentinfo landmark must be at top level
landmark-main-is-top-levelMain landmark must be at top level
landmark-no-duplicate-bannerDocument must not have more than one banner landmark
landmark-no-duplicate-contentinfoDocument must not have more than one contentinfo landmark
landmark-no-duplicate-mainDocument must not have more than one main landmark
landmark-one-mainDocument must have one main landmark
landmark-uniqueLandmarks must have unique labels
meta-viewport-largeViewport should allow significant zoom
page-has-heading-onePage should contain a level-one heading
presentation-role-conflictElements with conflicting ARIA on presentational role
regionAll page content must be contained within landmarks
scope-attr-validscope attribute values must be valid
skip-linkSkip links must have valid targets
tabindexElements should not have tabindex greater than 0
table-duplicate-nameTable <caption> and summary must not be identical

Part 4: axe-core Auto-Fix Engine (20 fixable violations)

The axe-core fix engine (axe-fixer.ts) runs a fix-then-re-audit loop up to 3 iterations with revert-on-regression safety.

Rule IDFix Strategy
regionWraps orphan body content in <section role="region" aria-label="Content">
color-contrastForces color: #1a1a1a !important; background-color: #ffffff !important on affected elements
heading-orderRemaps heading levels to sequential order (e.g., h1β†’h3 becomes h1β†’h2)
image-altAdds alt="Image" to images missing alt text
svg-img-altAdds aria-label="Mathematical expression" to SVGs with role="img"
role-img-altAdds aria-label="Image" to any non-SVG element with role="img" and no existing label
link-nameAdds aria-label="Link" to empty links
button-nameAdds aria-label="Button" to empty buttons
input-button-nameAdds value="Submit" to <input type="submit/button"> with no accessible name
labelAdds aria-label="Input field" to unlabeled inputs
label-title-onlyPromotes an existing title attribute to aria-label on form fields labeled only by title
select-nameAdds aria-label="Select an option" to <select> elements with no accessible name
document-titleAdds <title>Document</title> to <head>
html-has-langAdds lang="en" to <html>
landmark-one-mainWraps body content in <main role="main">
listWraps orphan <li> elements in <ul>
listitemSame as list β€” wraps orphan <li> elements in <ul>
landmark-uniqueAdds unique aria-label attributes to duplicate landmarks
meta-viewportAdds <meta name="viewport" content="width=device-width, initial-scale=1.0">
scrollable-region-focusableAdds tabindex="0" to scrollable regions

Fix loop behavior:

  1. Apply deterministic regex fixes for current violations
  2. Re-audit with axe-core (1.5s delay between attempts to avoid rate limits)
  3. If violation count increases β†’ revert and stop (regression safety)
  4. If violation count reaches 0 β†’ stop (all fixed)
  5. If no remaining fixable violations β†’ stop
  6. Repeat up to 3 times

Part 5: Rule Overlap Between Layers

Many rules are checked by both the custom validator and axe-core. The custom validator catches them first (and fixes them), so axe-core typically sees the already-fixed HTML.

RuleCustom Validatoraxe-coreNotes
document-titleCheck + fixCheck + fixCustom fixes first
html-has-langCheck + fixCheck + fixCustom fixes first
image-altCheck + fixCheck + fixCustom fixes first
link-nameCheck + fixCheck + fixCustom fixes first
button-nameCheck + fixCheck + fixCustom fixes first
labelCheck + fixCheck + fixCustom fixes first
heading-orderCheck + fixCheck + fixCustom fixes first
landmark-one-mainWarn + fixCheck + fixCustom fixes first
skip-linkWarn + fixCheck (target valid)Different scope
meta-viewportCheck onlyCheck + fixaxe-core also checks zoom
color-contrastCheck + fix (inline only)Full computed checkCustom fixes inline; axe-core handles cascade
empty-headingCheck + fixCheck (best practice)Custom fixes first
empty-table-headerWarn onlyCheck (best practice)Needs content knowledge β€” no fix
scope-attr-validCheck + fixCheck (best practice)Custom fixes first
list-structure / listCheck + fixCheck + fixCustom wraps non-li children; axe-core wraps orphan li
definition-listCheck + fixCheckCustom wraps invalid dl children
layout-tableCheck + fixβ€”Custom adds role=β€œpresentation”; axe-core does not check layout tables
status-messagesCheck + fixβ€”Custom adds aria-live; axe-core does not check this pattern
duplicate-idCheck + fixduplicate-id-ariaCustom fixes all; axe checks ARIA refs
table-has-headerCheck + fixth-has-data-cellsComplementary checks
aria-role-valid / aria-allowed-attrCheck (static)Check (computed)Custom does fast pre-pass; axe is authoritative
1.4.4 Resizemeta-viewport presence onlymeta-viewport, meta-viewport-largeFull zoom/reflow test via checkResizeText()
1.4.12 Text SpacingNot checked staticallyavoid-inline-spacing (partial)Full override test via checkTextSpacing()
2.4.3 Focus OrderNot checked staticallyfocus-order-semantics (partial)Full sequence analysis via checkFocusOrder()

Rules unique to axe-core (not in custom validator):

All frame/iframe rules, video/audio rules, target-size, autocomplete-valid, non-text contrast, aria-hidden-focus, scrollable-region-focusable, landmark-unique, nested-interactive, and many more.

Rules unique to custom validator (not in axe-core or browser checkers):

  • document-lang-valid β€” validates the lang attribute format
  • table-has-header β€” specifically checks for <th> presence (axe checks header-to-data-cell associations differently)
  • image-alt-meaningful β€” detects present-but-meaningless alt text (alt="image", filenames)
  • sensory-characteristics β€” pattern-matches instruction text for color/shape/location references
  • use-of-color β€” detects inline color styles without accompanying semantic indicators
  • layout-table β€” heuristic detection of tables used for visual layout rather than data
  • consistent-identification β€” detects same label on different element types across the document
  • lang-of-parts β€” detects non-Latin script characters outside a lang-annotated element

Part 6: What Cannot Be Checked

These WCAG success criteria are not evaluated by any layer of the pipeline. No violation is reported, no fix is applied, and no guidance is given during conversion. They require media playback, live user interaction, multi-touch gestures, device motion, or real form submission β€” none of which exist in static document conversion output.

WCAG SCLevelWhy not checked
1.2.1 Audio-only / Video-only (Prerecorded)ARequires inspecting or playing actual media files
1.2.2 Captions (Prerecorded)ARequires reading caption tracks from media
1.2.3 Audio Description or Media AlternativeARequires media playback
1.2.4 Captions (Live)AARequires live media stream
1.2.5 Audio Description (Prerecorded)AARequires media playback
1.4.11 Non-text ContrastAACovered by axe-core’s non-text-contrast rule β€” not in the table above because axe-core handles it automatically
1.4.13 Content on Hover or FocusAARequires triggering hover/focus states and observing popup behavior β€” not present in static PDF output
2.1.1 Keyboard (complete)Aaxe-core detects some keyboard traps; full manual keyboard navigation testing requires a human or specialized tool
2.1.2 No Keyboard TrapARequires entering every widget via keyboard and confirming focus can exit
2.3.1 Three Flashes or Below ThresholdARequires frame-by-frame animation rate measurement
2.4.7 Focus VisibleAACovered by axe-core’s focus-visible rule
2.5.1 Pointer GesturesANot applicable β€” PDF-to-HTML output has no multi-touch gestures
2.5.2 Pointer CancellationANot applicable β€” no mousedown/mouseup interaction flows in static output
2.5.3 Label in NameAPartially covered by axe-core’s label-content-name-mismatch rule
2.5.4 Motion ActuationANot applicable β€” static document output
3.2.1 On FocusARequires triggering focus events and observing unexpected context changes
3.2.2 On InputARequires triggering input events
3.3.1 Error IdentificationARequires submitting forms with invalid data β€” not present in static PDF output
3.3.3 Error SuggestionAASame as above
3.3.4 Error PreventionAARequires form submission with reversibility verification

Criteria not applicable to this product

These address behaviors that do not arise in static document conversion output.

WCAG SCLevelReason not applicable
1.3.4 OrientationAAConverted documents do not lock screen orientation
1.3.5 Identify Input PurposeAAautocomplete is for account/login forms, not converted document tables
2.1.4 Character Key ShortcutsAANo single-character shortcuts in converted output
2.2.1 Timing AdjustableANo time limits imposed on static documents
2.2.2 Pause, Stop, HideANo auto-updating or moving content
2.2.6 TimeoutsAAANo session timeouts in static output
2.4.5 Multiple WaysAASingle converted document; no site-level navigation
3.2.3 Consistent NavigationAANo multi-page navigation structure
3.2.5 Change on RequestAAANo dynamic context changes in static output
3.3.5 HelpAAAContext-sensitive help is a web-app concept
3.3.6 Error Prevention (All)AAANo user data submission in static output

Summary

MetricCustom Static ValidatorBrowser Custom Checkersaxe-core
Total rules / checks383~90+
Auto-fixable20No20
Flagged but not fixed19 (see βš‘ table above)5 failure types30+ rules
Execution time< 50ms1–2s each3–8s
Requires browserNoYes (Puppeteer)Yes (Puppeteer)
WCAG coverageA + AA + AAA1.4.4, 1.4.12, 2.4.3A through AAA + best practices
Fix strategyRegex + DOM replaceDetect onlyRegex replace + re-audit loop
Regression safetyIterates until stableN/AReverts if violations increase

Combined, the three layers provide comprehensive WCAG 2.1 AAA detection coverage. 20 custom validator rules and 20 axe-core rules are auto-fixed. The 19 rules marked βš‘ above are reported to the user but cannot be resolved without content knowledge, design changes, or media that is absent from the converted document.