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:
| Layer | Engine | Speed | Rules | Auto-Fix | Runs When |
|---|---|---|---|---|---|
| Custom Static Validator | Regex on HTML strings | < 50ms | 38 rules | 20 auto-fixable | Before saving HTML β fixes issues upstream |
| Browser Custom Checkers | Puppeteer + JS evaluation | 1β2s each | 3 checks | No | After conversion β checks layout/zoom/spacing/focus |
| axe-core Auditor | Browser-based (Puppeteer) | 3β8s | ~90+ rules | 20 auto-fixable | After 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 ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 1 | document-title | 2.4.2 | A | Yes | Document must have a <title> element |
| 2 | html-has-lang | 3.1.1 | A | Yes | <html> element must have a lang attribute |
| 3 | document-lang-valid | 3.1.1 | A | No | lang attribute value must be a valid BCP 47 code (e.g., en, fr-CA) |
| 4 | meta-viewport | 1.4.4 | AA | No | Viewport meta tag must be present |
| 5 | landmark-one-main | Best Practice | A | Yes | Document should have one <main> landmark |
| 6 | skip-link | 2.4.1 | A | Yes | Document should have a skip-to-content link |
Auto-fix details:
document-titleβ Inserts<title>Converted Document</title>into<head>.html-has-langβ Addslang="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 ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 7 | image-alt | 1.1.1 | A | Yes | <img> elements must have an alt attribute |
| 8 | image-alt-meaningful | 1.1.1 | A | No | alt attribute must not be a generic or filename-based value (alt="image", alt="photo", alt="img_001.png") |
| 9 | link-name | 2.4.4 | A | Yes | Links with href must have discernible text or aria-label |
| 10 | button-name | 4.1.2 | A | Yes | Buttons must have discernible text or aria-label |
| 11 | label | 1.3.1 | A | Yes | Form inputs (except hidden/submit/button/reset/image) must have associated labels |
| 12 | empty-heading | 1.3.1 | A | Yes | <h1> through <h6> elements must not be empty |
Auto-fix details:
image-altβ Addsalt="Image - description needed"to images missingalt.link-nameβ Addsaria-label="Link - description needed"to empty links.button-nameβ Addsaria-label="Button - description needed"to empty buttons.labelβ Addsaria-label="Input field - label needed"to unlabeled inputs.empty-headingβ Removes the empty heading element entirely.
Heading and Landmark Rules
| # | Rule ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 13 | heading-order | 1.3.1 | AA | Yes | Heading levels should increase sequentially (no jumps from h1 to h3) |
| 14 | heading-descriptive | 2.4.6 | AA | No β | Heading text must be descriptive; generic values (βSection 2β, βContinuedβ, purely numeric) are flagged as warnings |
Table Rules
| # | Rule ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 15 | table-has-header | 1.3.1 | A | Yes | <table> elements must contain at least one <th> |
| 16 | empty-table-header | 1.3.1 | A | No β | <th> elements must not be empty |
| 17 | scope-attr-valid | 1.3.1 | A | Yes | scope attribute must be row, col, rowgroup, or colgroup |
| 18 | th-has-scope | 1.3.1 | A | No β | Every <th> must have a scope attribute |
| 19 | layout-table | 1.3.2 | A | Yes | Tables 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 invalidscopeattributes.layout-tableβ Addsrole="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 ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 20 | list-structure | 1.3.1 | A | Yes | <ul> and <ol> must only contain <li> as direct children; orphan <li> outside lists is flagged |
| 21 | definition-list | 1.3.1 | A | Yes | <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 ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 22 | duplicate-id | 4.1.1 | A | Yes | Element id attributes must be unique within the document |
| 23 | invalid-nesting | 4.1.1 | A | No | Block elements (<div>, <p>, <ul>) inside inline elements (<span>, <a>) is invalid HTML; anchor-inside-anchor and interactive-inside-interactive are also flagged |
| 24 | aria-role-valid | 4.1.2 | A | Yes | role attribute values must be from the ARIA specification (e.g., role="dropdown" is invalid) |
| 25 | aria-allowed-attr | 4.1.2 | A | No β | aria-* state/property attributes must be compatible with the elementβs role (e.g., aria-checked on a plain <div>) |
| 26 | status-messages | 4.1.3 | AA | Yes | Documents 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 sameid. Also updates anyaria-labelledby,aria-describedby,aria-controls,aria-owns,aria-flowto,aria-activedescendant, andforattributes that referenced the renamed ID.aria-role-validβ Removes the invalidroleattribute 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 matchingalert|error|notice|status|notification|message|feedback), addsaria-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 ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 27 | color-contrast | 1.4.3 | AA | Yes (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. |
| 28 | use-of-color | 1.4.1 | A | No β | Inline color styles on short text elements without a semantic indicator or icon are flagged as possible color-only information (warning) |
| 29 | sensory-characteristics | 1.3.3 | A | No β | Instructions referencing shape, color, size, or location only (βclick the green buttonβ, βsee the box on the rightβ) are flagged as warnings |
| 30 | images-of-text | 1.4.5 | AA | No β | <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:
| Element | Foreground | Background | Ratio | AA (4.5:1) | AAA (7:1) |
|---|---|---|---|---|---|
| Body text | #1a1a1a | #ffffff | 17.4:1 | Pass | Pass |
| Figcaption | #555555 | #ffffff | 7.5:1 | Pass | Pass |
| Secondary text | #4a4a68 | #ffffff | 7.0:1 | Pass | Pass |
| Heading text | #1a1a2e | #ffffff | 16.0:1 | Pass | Pass |
| Source header | #495057 | #f8f9fa | 7.0:1 | Pass | Pass |
Scope limitation: Does not resolve CSS cascade, var(), inherit, currentColor, or external stylesheets β those remain axe-coreβs job.
Language Rules
| # | Rule ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 31 | lang-of-parts | 3.1.2 | AA | No β | 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 |
Navigation and Identification Rules
| # | Rule ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 32 | consistent-identification | 3.2.4 | AA | No β | 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 ID | WCAG SC | Level | Auto-Fix | Description |
|---|---|---|---|---|---|
| 33 | color-contrast-enhanced | 1.4.6 | AAA | No β | Text must achieve a 7:1 contrast ratio (3:1 for large text) β checked only against known injected color pairs |
| 34 | visual-presentation | 1.4.8 | AAA | Yes | text-align: justify and user-select: none are flagged and removed from all inline styles and <style> blocks |
| 35 | images-of-text-no-exception | 1.4.9 | AAA | No β | <img> elements whose alt text contains >20 characters of prose are flagged as likely images of text |
| 36 | link-purpose-sole | 2.4.9 | AAA | No β | Links with identical text that point to different destinations are flagged as ambiguous |
| 37 | section-headings | 2.4.10 | AAA | No β | Documents with more than one long text block and no headings are flagged for missing section headings |
| 38 | abbreviation-expansion | 3.1.4 | AAA | No β | Common abbreviations not wrapped in <abbr title="..."> are flagged as warnings |
Auto-fix detail β visual-presentation:
- Removes
text-align: justifyfrom all inlinestyle="..."attributes and<style>blocks (justified text creates uneven word-spacing that impacts readability for users with dyslexia). - Removes
user-select: nonefrom all inlinestyle="..."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 ID | WCAG SC | Level | Why Not Fixed |
|---|---|---|---|
document-lang-valid | 3.1.1 | A | Cannot determine the correct BCP 47 language code from context |
image-alt-meaningful | 1.1.1 | A | Requires understanding what the image depicts β needs AI vision or human review |
empty-table-header | 1.3.1 | A | Cannot supply the missing header label without knowing what column it represents |
th-has-scope (complex tables) | 1.3.1 | A | Structural scope assignment in complex multi-row/col-span tables requires human review |
aria-allowed-attr | 4.1.2 | A | Incompatible ARIA attribute is reported as a warning β strip would be unsafe without knowing intended widget pattern |
invalid-nesting | 4.1.1 | A | Block-in-inline and anchor-inside-anchor detected; restructuring may change document meaning |
meta-viewport (zoom disabled) | 1.4.4 | AA | Presence checked; if user-scalable=no is intentional, not auto-removed |
heading-descriptive | 2.4.6 | AA | Rewriting heading content requires understanding what section the heading introduces |
use-of-color | 1.4.1 | A | Prose rewrite or icon addition required β cannot add semantic meaning automatically |
sensory-characteristics | 1.3.3 | A | Prose rewrite required β cannot change instruction wording without content understanding |
images-of-text | 1.4.5 | AA | Cannot convert a rasterized image to actual text |
lang-of-parts | 3.1.2 | AA | Script detected but correct BCP 47 code cannot be inferred without a language-detection service |
consistent-identification | 3.2.4 | AA | May be intentional β requires content review to determine if inconsistency is a defect |
color-contrast (CSS cascade) | 1.4.3 | AA | Failures caused by external CSS, var(), inherit, or currentColor cannot be resolved by static HTML analysis |
color-contrast-enhanced | 1.4.6 | AAA | Inline contrast corrected to AA level only; achieving 7:1 may not be possible without design changes |
images-of-text-no-exception | 1.4.9 | AAA | Cannot convert image to text |
link-purpose-sole | 2.4.9 | AAA | Same-text links with different destinations require content-level rewrites |
section-headings | 2.4.10 | AAA | Cannot add headings without knowing where section boundaries belong |
abbreviation-expansion | 3.1.4 | AAA | Cannot 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 > innerWidthindicates content does not reflow and requires horizontal scrolling at high zoom. - Clipped content β elements with
overflow: hiddenwhosescrollWidth > clientWidth + 2pxafter 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:
- Collects all focusable elements:
a[href],button,input,select,textarea,[tabindex] - Identifies missed elements: interactive elements with
tabindex="-1"(explicitly removed from tab order β a problem if there is no alternative keyboard path) - Builds the theoretical tab sequence: elements with positive
tabindexvalues come first (ascending), then elements withtabindex="0"in DOM order - Warns when an elementβs tab position differs from its DOM position by more than 2 β indicating a positive
tabindexis 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-practiceThis 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 ID | Description | WCAG SC |
|---|---|---|
area-alt | Image map <area> elements must have alternate text | 1.1.1 |
aria-allowed-attr | ARIA attributes must be appropriate for the elementβs role | 4.1.2 |
aria-braille-equivalent | aria-braillelabel and aria-brailleroledescription must have non-braille equivalent | 4.1.2 |
aria-command-name | ARIA buttons, links, and menuitems must have accessible names | 4.1.2 |
aria-conditional-attr | ARIA attributes must be used correctly for the current state | 4.1.2 |
aria-deprecated-role | Deprecated ARIA roles must not be used | 4.1.2 |
aria-hidden-body | aria-hidden="true" must not be present on <body> | 4.1.2 |
aria-hidden-focus | aria-hidden elements must not contain focusable elements | 4.1.2 |
aria-input-field-name | ARIA input fields must have accessible names | 4.1.2 |
aria-meter-name | ARIA meter elements must have accessible names | 1.1.1 |
aria-progressbar-name | ARIA progressbar elements must have accessible names | 1.1.1 |
aria-prohibited-attr | ARIA attributes must not be prohibited for the elementβs role | 4.1.2 |
aria-required-attr | Required ARIA attributes must be present | 4.1.2 |
aria-required-children | ARIA roles must contain required child roles | 4.1.2 |
aria-required-parent | ARIA roles must be contained by required parent roles | 4.1.2 |
aria-roles | ARIA role values must be valid | 4.1.2 |
aria-toggle-field-name | ARIA toggle fields must have accessible names | 4.1.2 |
aria-tooltip-name | ARIA tooltip elements must have accessible names | 4.1.2 |
aria-valid-attr-value | ARIA attribute values must be valid | 4.1.2 |
aria-valid-attr | ARIA attribute names must be valid | 4.1.2 |
blink | <blink> elements must not be used | 2.2.2 |
button-name | Buttons must have discernible text | 4.1.2 |
bypass | Pages must have a mechanism to bypass repeated content | 2.4.1 |
definition-list | <dl> elements must be structured correctly | 1.3.1 |
dlitem | <dt> and <dd> must be contained by a <dl> | 1.3.1 |
document-title | Documents must have a <title> element | 2.4.2 |
duplicate-id-aria | IDs used in ARIA and labels must be unique | 4.1.1 |
form-field-multiple-labels | Form fields must not have multiple labels | 1.3.1 |
frame-focusable-content | Frames with focusable content must not have tabindex="-1" | 2.1.1 |
frame-title-unique | <iframe> and <frame> elements must have unique titles | 4.1.2 |
frame-title | Frames must have accessible names | 4.1.2 |
html-has-lang | <html> element must have a lang attribute | 3.1.1 |
html-lang-valid | <html> element lang attribute must be valid | 3.1.1 |
html-xml-lang-mismatch | lang and xml:lang must match | 3.1.1 |
image-alt | Images must have alternate text | 1.1.1 |
input-button-name | Input buttons must have discernible text | 4.1.2 |
input-image-alt | <input type="image"> must have alternate text | 1.1.1 |
label | Form elements must have labels | 1.3.1 |
link-in-text-block | Links must be distinguishable without relying on color | 1.4.1 |
link-name | Links must have discernible text | 2.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 used | 2.2.2 |
meta-refresh | Timed meta refresh must not be used | 2.2.1 |
nested-interactive | Interactive controls must not be nested | 4.1.2 |
no-autoplay-audio | Audio must not autoplay for more than 3 seconds | 1.4.2 |
object-alt | <object> elements must have alternate text | 1.1.1 |
role-img-alt | Elements with role="img" must have alternate text | 1.1.1 |
scrollable-region-focusable | Scrollable regions must be keyboard accessible | 2.1.1 |
select-name | <select> elements must have accessible names | 1.3.1 |
server-side-image-map | Server-side image maps must not be used | 2.1.1 |
summary-name | <summary> elements must have discernible text | 4.1.2 |
svg-img-alt | SVG elements with role="img" must have alternate text | 1.1.1 |
td-headers-attr | headers attribute values must refer to cells in the same table | 1.3.1 |
th-has-data-cells | Table headers must be associated with data cells | 1.3.1 |
video-caption | <video> elements must have captions | 1.2.2 |
WCAG 2.0/2.1/2.2 Level AA Rules
| Rule ID | Description | WCAG SC |
|---|---|---|
color-contrast | Text must have sufficient color contrast (4.5:1 normal, 3:1 large) | 1.4.3 |
valid-lang | lang attribute values must be valid | 3.1.2 |
meta-viewport | Viewport must not disable text scaling | 1.4.4 |
autocomplete-valid | autocomplete attribute values must be valid | 1.3.5 |
avoid-inline-spacing | Inline text spacing must be adjustable | 1.4.12 |
target-size | Touch targets must be at least 24x24 CSS pixels | 2.5.8 |
WCAG 2.0/2.1/2.2 Level AAA Rules
| Rule ID | Description | WCAG SC |
|---|---|---|
color-contrast-enhanced | Text must have enhanced color contrast (7:1 normal, 4.5:1 large) | 1.4.6 |
identical-links-same-purpose | Links with identical text must serve the same purpose | 2.4.9 |
meta-refresh-no-exceptions | Timed meta refresh must not be used (no exceptions) | 2.2.1 |
Best Practice Rules
| Rule ID | Description |
|---|---|
accesskeys | accesskey attribute values must be unique |
aria-allowed-role | ARIA roles must be appropriate for the element |
aria-dialog-name | ARIA dialog and alertdialog must have accessible names |
aria-text | role="text" must be used correctly |
aria-treeitem-name | ARIA treeitem elements must have accessible names |
empty-heading | Headings must not be empty |
empty-table-header | Table header cells must not be empty |
heading-order | Heading levels should increase sequentially |
image-redundant-alt | Image alt text must not duplicate surrounding text |
label-title-only | Form elements should not use title as only label |
landmark-banner-is-top-level | Banner landmark must be at top level |
landmark-complementary-is-top-level | Complementary landmark must be at top level |
landmark-contentinfo-is-top-level | Contentinfo landmark must be at top level |
landmark-main-is-top-level | Main landmark must be at top level |
landmark-no-duplicate-banner | Document must not have more than one banner landmark |
landmark-no-duplicate-contentinfo | Document must not have more than one contentinfo landmark |
landmark-no-duplicate-main | Document must not have more than one main landmark |
landmark-one-main | Document must have one main landmark |
landmark-unique | Landmarks must have unique labels |
meta-viewport-large | Viewport should allow significant zoom |
page-has-heading-one | Page should contain a level-one heading |
presentation-role-conflict | Elements with conflicting ARIA on presentational role |
region | All page content must be contained within landmarks |
scope-attr-valid | scope attribute values must be valid |
skip-link | Skip links must have valid targets |
tabindex | Elements should not have tabindex greater than 0 |
table-duplicate-name | Table <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 ID | Fix Strategy |
|---|---|
region | Wraps orphan body content in <section role="region" aria-label="Content"> |
color-contrast | Forces color: #1a1a1a !important; background-color: #ffffff !important on affected elements |
heading-order | Remaps heading levels to sequential order (e.g., h1βh3 becomes h1βh2) |
image-alt | Adds alt="Image" to images missing alt text |
svg-img-alt | Adds aria-label="Mathematical expression" to SVGs with role="img" |
role-img-alt | Adds aria-label="Image" to any non-SVG element with role="img" and no existing label |
link-name | Adds aria-label="Link" to empty links |
button-name | Adds aria-label="Button" to empty buttons |
input-button-name | Adds value="Submit" to <input type="submit/button"> with no accessible name |
label | Adds aria-label="Input field" to unlabeled inputs |
label-title-only | Promotes an existing title attribute to aria-label on form fields labeled only by title |
select-name | Adds aria-label="Select an option" to <select> elements with no accessible name |
document-title | Adds <title>Document</title> to <head> |
html-has-lang | Adds lang="en" to <html> |
landmark-one-main | Wraps body content in <main role="main"> |
list | Wraps orphan <li> elements in <ul> |
listitem | Same as list β wraps orphan <li> elements in <ul> |
landmark-unique | Adds unique aria-label attributes to duplicate landmarks |
meta-viewport | Adds <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
scrollable-region-focusable | Adds tabindex="0" to scrollable regions |
Fix loop behavior:
- Apply deterministic regex fixes for current violations
- Re-audit with axe-core (1.5s delay between attempts to avoid rate limits)
- If violation count increases β revert and stop (regression safety)
- If violation count reaches 0 β stop (all fixed)
- If no remaining fixable violations β stop
- 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.
| Rule | Custom Validator | axe-core | Notes |
|---|---|---|---|
document-title | Check + fix | Check + fix | Custom fixes first |
html-has-lang | Check + fix | Check + fix | Custom fixes first |
image-alt | Check + fix | Check + fix | Custom fixes first |
link-name | Check + fix | Check + fix | Custom fixes first |
button-name | Check + fix | Check + fix | Custom fixes first |
label | Check + fix | Check + fix | Custom fixes first |
heading-order | Check + fix | Check + fix | Custom fixes first |
landmark-one-main | Warn + fix | Check + fix | Custom fixes first |
skip-link | Warn + fix | Check (target valid) | Different scope |
meta-viewport | Check only | Check + fix | axe-core also checks zoom |
color-contrast | Check + fix (inline only) | Full computed check | Custom fixes inline; axe-core handles cascade |
empty-heading | Check + fix | Check (best practice) | Custom fixes first |
empty-table-header | Warn only | Check (best practice) | Needs content knowledge β no fix |
scope-attr-valid | Check + fix | Check (best practice) | Custom fixes first |
list-structure / list | Check + fix | Check + fix | Custom wraps non-li children; axe-core wraps orphan li |
definition-list | Check + fix | Check | Custom wraps invalid dl children |
layout-table | Check + fix | β | Custom adds role=βpresentationβ; axe-core does not check layout tables |
status-messages | Check + fix | β | Custom adds aria-live; axe-core does not check this pattern |
duplicate-id | Check + fix | duplicate-id-aria | Custom fixes all; axe checks ARIA refs |
table-has-header | Check + fix | th-has-data-cells | Complementary checks |
aria-role-valid / aria-allowed-attr | Check (static) | Check (computed) | Custom does fast pre-pass; axe is authoritative |
1.4.4 Resize | meta-viewport presence only | meta-viewport, meta-viewport-large | Full zoom/reflow test via checkResizeText() |
1.4.12 Text Spacing | Not checked statically | avoid-inline-spacing (partial) | Full override test via checkTextSpacing() |
2.4.3 Focus Order | Not checked statically | focus-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 formattable-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 referencesuse-of-colorβ detects inlinecolorstyles without accompanying semantic indicatorslayout-tableβ heuristic detection of tables used for visual layout rather than dataconsistent-identificationβ detects same label on different element types across the documentlang-of-partsβ detects non-Latin script characters outside alang-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 SC | Level | Why not checked |
|---|---|---|
| 1.2.1 Audio-only / Video-only (Prerecorded) | A | Requires inspecting or playing actual media files |
| 1.2.2 Captions (Prerecorded) | A | Requires reading caption tracks from media |
| 1.2.3 Audio Description or Media Alternative | A | Requires media playback |
| 1.2.4 Captions (Live) | AA | Requires live media stream |
| 1.2.5 Audio Description (Prerecorded) | AA | Requires media playback |
| 1.4.11 Non-text Contrast | AA | Covered 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 Focus | AA | Requires triggering hover/focus states and observing popup behavior β not present in static PDF output |
| 2.1.1 Keyboard (complete) | A | axe-core detects some keyboard traps; full manual keyboard navigation testing requires a human or specialized tool |
| 2.1.2 No Keyboard Trap | A | Requires entering every widget via keyboard and confirming focus can exit |
| 2.3.1 Three Flashes or Below Threshold | A | Requires frame-by-frame animation rate measurement |
| 2.4.7 Focus Visible | AA | Covered by axe-coreβs focus-visible rule |
| 2.5.1 Pointer Gestures | A | Not applicable β PDF-to-HTML output has no multi-touch gestures |
| 2.5.2 Pointer Cancellation | A | Not applicable β no mousedown/mouseup interaction flows in static output |
| 2.5.3 Label in Name | A | Partially covered by axe-coreβs label-content-name-mismatch rule |
| 2.5.4 Motion Actuation | A | Not applicable β static document output |
| 3.2.1 On Focus | A | Requires triggering focus events and observing unexpected context changes |
| 3.2.2 On Input | A | Requires triggering input events |
| 3.3.1 Error Identification | A | Requires submitting forms with invalid data β not present in static PDF output |
| 3.3.3 Error Suggestion | AA | Same as above |
| 3.3.4 Error Prevention | AA | Requires form submission with reversibility verification |
Criteria not applicable to this product
These address behaviors that do not arise in static document conversion output.
| WCAG SC | Level | Reason not applicable |
|---|---|---|
| 1.3.4 Orientation | AA | Converted documents do not lock screen orientation |
| 1.3.5 Identify Input Purpose | AA | autocomplete is for account/login forms, not converted document tables |
| 2.1.4 Character Key Shortcuts | AA | No single-character shortcuts in converted output |
| 2.2.1 Timing Adjustable | A | No time limits imposed on static documents |
| 2.2.2 Pause, Stop, Hide | A | No auto-updating or moving content |
| 2.2.6 Timeouts | AAA | No session timeouts in static output |
| 2.4.5 Multiple Ways | AA | Single converted document; no site-level navigation |
| 3.2.3 Consistent Navigation | AA | No multi-page navigation structure |
| 3.2.5 Change on Request | AAA | No dynamic context changes in static output |
| 3.3.5 Help | AAA | Context-sensitive help is a web-app concept |
| 3.3.6 Error Prevention (All) | AAA | No user data submission in static output |
Summary
| Metric | Custom Static Validator | Browser Custom Checkers | axe-core |
|---|---|---|---|
| Total rules / checks | 38 | 3 | ~90+ |
| Auto-fixable | 20 | No | 20 |
| Flagged but not fixed | 19 (see β table above) | 5 failure types | 30+ rules |
| Execution time | < 50ms | 1β2s each | 3β8s |
| Requires browser | No | Yes (Puppeteer) | Yes (Puppeteer) |
| WCAG coverage | A + AA + AAA | 1.4.4, 1.4.12, 2.4.3 | A through AAA + best practices |
| Fix strategy | Regex + DOM replace | Detect only | Regex replace + re-audit loop |
| Regression safety | Iterates until stable | N/A | Reverts 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.