Upgrading the Custom Validator to WCAG 2.2 AAA
Current State
The custom regex-based validator in workers/api/src/services/wcag-validator.ts implements 12 rules targeting WCAG 2.1 AA. Eight of these rules have auto-fix capability.
| Rule ID | WCAG SC | Level | Auto-Fix |
|---|---|---|---|
document-title | 2.4.2 | A | Yes |
html-has-lang | 3.1.1 | A | Yes |
image-alt | 1.1.1 | A | Yes |
link-name | 2.4.4 | A | Yes |
button-name | 4.1.2 | A | Yes |
label | 1.3.1 | A | Yes |
heading-order | 1.3.1 | AA | No |
landmark-one-main | β | Best practice | Yes |
skip-link | 2.4.1 | A | Yes |
meta-viewport | 1.4.4 | AA | No |
color-contrast | 1.4.3 | AA | No (stub) |
document-lang-valid | 3.1.1 | A | No |
The validator operates entirely on HTML strings using regex pattern matching. It has no DOM, no computed styles, and no layout engine.
Gap Analysis: What WCAG 2.2 AAA Requires
WCAG 2.2 AAA adds criteria on top of 2.0/2.1 AA. The full conformance surface includes every A, AA, and AAA success criterion from WCAG 2.0, 2.1, and 2.2 β roughly 86 success criteria total. The custom validator currently covers 10 of those (plus 2 best-practice rules).
The gap breaks down into three tiers based on implementation feasibility.
Tier 1: Regex-Feasible (~6 rules, ~2 days)
Rules that can be checked with regex or simple string analysis against the HTML source.
| Rule | WCAG SC | Level | What It Checks |
|---|---|---|---|
table-has-header | 1.3.1 | A | <table> elements contain <th> or scope attributes |
aria-valid-attr | 4.1.2 | A | aria-* attributes are from the spec-defined set |
aria-required-attr | 4.1.2 | A | ARIA roles have their required attributes (e.g., role="checkbox" needs aria-checked) |
definition-list | 1.3.1 | A | <dl> only contains <dt> and <dd> children |
list-structure | 1.3.1 | A | <ul>/<ol> only contain <li> children |
duplicate-id | 4.1.1 | A | No repeated id values in the document |
Auto-fix potential: table-has-header (promote first row to <th>), duplicate-id (append suffix). Others are detect-only.
Effort: ~2 developer-days. These are straightforward pattern-match checks with well-defined pass/fail criteria.
Tier 2: Stateful or Computed (~12 rules, ~5 days)
Rules that need more than simple regex β color math, multi-pass analysis, or attribute cross-referencing β but still donβt require a browser.
| Rule | WCAG SC | Level | What It Checks | Challenge |
|---|---|---|---|---|
color-contrast (real) | 1.4.3 / 1.4.6 | AA / AAA | Foreground-to-background contrast ratio >= 4.5:1 (AA) or 7:1 (AAA) | Parse inline styles + CSS, resolve inherit, compute relative luminance |
color-contrast-enhanced | 1.4.6 | AAA | Same as above at 7:1 ratio for normal text, 4.5:1 for large text | Same color math, stricter thresholds |
autocomplete-valid | 1.3.5 | AA | autocomplete attribute values are valid tokens | Validate against the HTML spec token list |
aria-allowed-role | 4.1.2 | A | Elements only use ARIA roles appropriate for their tag | Map of tag-to-allowed-roles |
aria-hidden-focus | 4.1.2 | A | Elements with aria-hidden="true" are not focusable | Track tabindex, href, <button>, etc. inside hidden subtrees |
form-field-multiple-labels | 1.3.1 | A | No input has more than one associated <label> | Cross-reference for attributes and nesting |
identical-links-same-purpose | 2.4.9 | AAA | Links with same text point to same URL | Aggregate link text-to-href mapping |
link-in-text-block | 1.4.1 | A | Links in text blocks are distinguishable by more than color alone | Detect underline/border in inline styles |
page-has-heading-one | 1.3.1 | A | Page has at least one <h1> | Simple but included here due to needing heading hierarchy context |
empty-heading | 1.3.1 | A | Headings are not empty | Regex for <h[1-6]> with no text content |
empty-table-header | 1.3.1 | A | <th> elements have content | Regex for <th> with no text |
scope-attr-valid | 1.3.1 | A | scope attribute is row, col, rowgroup, or colgroup | Attribute value validation |
Auto-fix potential: empty-heading (remove empty headings), scope-attr-valid (remove invalid scope), page-has-heading-one (promote first heading). Color contrast auto-fix is impractical without a design system.
Effort: ~5 developer-days. Color contrast alone is ~2 days (CSS parsing, color resolution, luminance math). The ARIA rules need a role-to-element mapping table.
Tier 3: Browser-Required (~20+ rules, impractical without a DOM)
These rules require computed styles, layout information, focus management, or runtime behavior that only a real browser can provide. This is exactly what axe-core does.
| Category | Example Rules | WCAG SC | Why It Needs a Browser |
|---|---|---|---|
| Focus management | focus-order-semantics, tabindex | 2.4.3 (A) | Requires sequential focus navigation testing |
| Target size | target-size | 2.5.8 (AA), 2.5.5 (AAA) | Needs computed bounding box of interactive elements |
| Reflow | meta-viewport-large | 1.4.10 (AA) | Requires rendering at different viewport widths |
| Text spacing | text-spacing | 1.4.12 (AA) | Needs computed styles after CSS cascade |
| Animation | prefers-reduced-motion | 2.3.3 (AAA) | Requires detecting animated content |
| Computed contrast | CSS variables, currentColor, gradients | 1.4.3 / 1.4.6 | Needs resolved computed styles |
| Visible labels | label-content-name-mismatch | 2.5.3 (A) | Needs computed accessible name vs. visible text |
| Orientation | css-orientation-lock | 1.3.4 (AA) | Needs media query evaluation |
| Dragging | Dragging alternatives | 2.5.7 (AA) | Needs event handler inspection |
| Focus appearance | Focus indicator visibility | 2.4.11 (AA), 2.4.12 (AAA) | Needs computed outline/border around focused elements |
| Consistent navigation | Navigation consistency across pages | 3.2.3 (AA) | Requires multi-page comparison |
| Error identification | Form error messages | 3.3.1 (A) | Requires form submission behavior |
| Status messages | Live region announcements | 4.1.3 (AA) | Requires aria-live runtime behavior |
Auto-fix potential: Minimal. These are structural/behavioral issues.
Effort: Impractical to implement in a regex-based validator. Each rule would require embedding a browser or DOM library (jsdom, linkedom, etc.), at which point youβve rebuilt axe-core.
Effort Summary
| Tier | Rules | Effort | Auto-Fixable | Notes |
|---|---|---|---|---|
| 1. Regex-feasible | ~6 | ~2 days | 2-3 | Simple string pattern checks |
| 2. Stateful/computed | ~12 | ~5 days | 3-4 | Color math, cross-references |
| 3. Browser-required | ~20+ | Impractical | ~0 | Needs layout engine |
| Total gap | ~38+ | ~7 days (Tier 1+2) | 5-7 |
Coverage after Tier 1+2: ~24 of ~86 SC (28% of WCAG 2.2 AAA).
Coverage with axe-core (already integrated): 90+ rules, ~70% of WCAG 2.2 AAA with zero additional effort.
Recommendation
Donβt expand the custom validator significantly. The two layers now serve complementary roles:
| Layer | Role | Speed | Coverage |
|---|---|---|---|
| Custom validator | Fast auto-fixing of 8 common issues | < 50ms | 12 rules, 8 fixable |
| axe-core (now on every file) | Comprehensive browser-based audit | 3-8s | 90+ rules |
The highest-value work on the custom validator is:
- Tier 1 rules (~2 days) β
table-has-header,duplicate-id,aria-valid-attr, list structure checks. These are cheap to add and several are auto-fixable. - Real color-contrast checking (~2 days) β Replace the stub that always passes. Parse inline styles and our injected CSS to compute actual contrast ratios. Wonβt catch CSS-variable or cascaded colors, but covers the majority of converted HTML output.
- Leave Tier 3 to axe-core β Anything requiring layout, focus, or computed styles is exactly what the browser-based audit provides.
This yields ~20 custom rules with ~10 auto-fixable at a cost of ~4 days, while axe-core handles the remaining 70+ rules that require a browser.