Skip to content

WCAG Validation Rules β€” Complete Reference

This document describes every accessibility rule checked by both validation layers in the Accessible PDF Converter pipeline: the custom regex-based validator and the axe-core browser-based auditor.


Architecture Overview

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

LayerEngineSpeedRulesAuto-FixRuns When
Custom ValidatorRegex on HTML strings< 50ms20 rules12 auto-fixableBefore saving HTML β€” fixes issues upstream
axe-core AuditorBrowser-based (Puppeteer)3–8s~90+ rules16 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. 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
  • axe-core auditor: workers/api/src/services/axe-validator.ts
  • axe-core fixer: workers/api/src/services/axe-fixer.ts

Part 1: Custom Validator Rules (20 rules)

The custom validator operates entirely on HTML strings using regex pattern matching. It has no DOM, no computed styles, and no layout engine. 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
8link-name2.4.4AYesLinks with href must have discernible text or aria-label
9button-name4.1.2AYesButtons must have discernible text or aria-label
10label1.3.1AYesForm inputs (except hidden/submit/button/reset/image) must have associated labels
11empty-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
12heading-order1.3.1AANoHeading levels should increase sequentially (no jumps from h1 to h3)

Table Rules

#Rule IDWCAG SCLevelAuto-FixDescription
13table-has-header1.3.1AYes<table> elements must contain at least one <th>
14empty-table-header1.3.1ANo<th> elements must not be empty
15scope-attr-valid1.3.1AYesscope attribute must be row, col, rowgroup, or colgroup

Auto-fix details:

  • table-has-header β€” Promotes <td> cells in the first <tr> to <th scope="col">.
  • scope-attr-valid β€” Removes invalid scope attributes.

List Rules

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

ID Uniqueness

#Rule IDWCAG SCLevelAuto-FixDescription
18duplicate-id4.1.1AYesElement id attributes must be unique within the document

Auto-fix details:

  • duplicate-id β€” Appends -2, -3, etc. suffix to second and subsequent occurrences of the same id.

Color Contrast

#Rule IDWCAG SCLevelAuto-FixDescription
19–20color-contrast1.4.3 / 1.4.6AA / AAANoText must have sufficient contrast ratio against its background

The color-contrast rule performs real WCAG luminance calculations using workers/api/src/utils/color.ts:

Known injected color pairs checked:

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

Inline style checking: Elements with style attributes containing both color and background-color (or background) are parsed and checked.

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


Part 2: 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

These rules test the most fundamental accessibility requirements.

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

These are not formal WCAG requirements but are widely recommended for accessibility.

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 3: axe-core Auto-Fix Engine (16 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"
link-nameAdds aria-label="Link" to empty links
button-nameAdds aria-label="Button" to empty buttons
labelAdds aria-label="Input field" to unlabeled inputs
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 4: Rule Overlap Between Layers

Many rules are checked by both layers. 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-orderWarn onlyCheck + fixaxe-core can remap levels
landmark-one-mainWarn + fixCheck + fixCustom fixes first
skip-linkWarn + fixCheck (target valid)Different scope
meta-viewportCheck onlyCheck + fixaxe-core also checks zoom
color-contrastCheck (inline only)Full computed checkaxe-core handles cascade
empty-headingCheck + fixCheck (best practice)Custom fixes first
empty-table-headerWarn onlyCheck (best practice)Needs content knowledge
scope-attr-validCheck + fixCheck (best practice)Custom fixes first
list-structure / listWarn onlyCheck + fixaxe-core can wrap orphans
definition-listWarn onlyCheckBoth detect-only in custom
duplicate-idCheck + fixduplicate-id-ariaSlightly different scope
table-has-headerCheck + fixth-has-data-cellsComplementary checks

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

All ARIA validation rules, frame/iframe rules, video/audio rules, target-size, autocomplete-valid, avoid-inline-spacing, color-contrast-enhanced, region, scrollable-region-focusable, landmark-unique, nested-interactive, and many more.

Rules unique to custom validator:

  • document-lang-valid β€” validates the lang attribute format
  • table-has-header β€” specifically checks for <th> presence (axe checks header-to-data-cell associations differently)

Part 5: What Cannot Be Checked Without a Browser

The following WCAG success criteria require runtime behavior, computed styles, or layout information that neither regex nor static analysis can provide. These are exclusively axe-core’s domain:

CategoryExample RulesWCAG SCWhy Browser Required
Focus managementfocus-order-semantics, tabindex2.4.3Requires sequential focus navigation
Target sizetarget-size2.5.8Needs computed bounding box
Reflowmeta-viewport-large1.4.10Requires rendering at different widths
Text spacingavoid-inline-spacing1.4.12Needs computed styles after cascade
Computed contrastCSS variables, currentColor, gradients1.4.3 / 1.4.6Needs resolved computed styles
Visible labelslabel-content-name-mismatch2.5.3Needs computed accessible name vs. visible text
Scrollable regionsscrollable-region-focusable2.1.1Needs overflow detection

Summary

MetricCustom Validatoraxe-core
Total rules20~90+
Auto-fixable1216
Execution time< 50ms3–8s
Requires browserNoYes
WCAG coverageA + some AAA through AAA + best practices
Fix strategyRegex replaceRegex replace + re-audit loop
Regression safetyIterates until stableReverts if violations increase

Combined, the two layers provide comprehensive WCAG 2.2 AAA coverage with aggressive upstream auto-fixing.