Path: blob/main/llm-docs/axe-accessibility-architecture.md
6832 views
------Axe Accessibility Checking Architecture
Quarto's axe-core integration spans three layers: build-time TypeScript, compile-time SCSS, and runtime JavaScript. Each layer operates at a fundamentally different stage, which is why they detect formats differently.
Three-Layer Overview
Layer 1: Build-time TypeScript (format detection + dependency injection)
File: src/format/html/format-html.ts
TypeScript code detects the output format and conditionally injects axe-check.js + axe-check.css as a FormatDependency. The format detection uses the Quarto format system (isHtmlOutput, isRevealJsOutput, isDashboardOutput).
Key responsibilities:
Read
axeoption from document metadataInject axe files as
FormatDependency(copies JS/CSS into output)Encode options as base64 JSON into a
<script>tag
The base64 encoding is a defensive pattern that prevents JSON containing </script> from breaking the HTML parser.
Layer 2: Compile-time SCSS (format-specific styling)
Files:
src/resources/formats/html/axe/axe-check.scss— HTML/dashboard stylessrc/resources/formats/revealjs/axe/axe-check.scss— RevealJS-specific styles
SCSS files provide format-specific visual styling. RevealJS compiles its sass-bundles separately from the theme, so theme variables aren't in scope. The CSS custom property bridge (--quarto-axe-* variables set in HTML themes, consumed in RevealJS via var() with !default fallbacks) works around this architectural constraint.
Layer 3: Runtime JavaScript (scanning + reporting)
File: src/resources/formats/html/axe/axe-check.js
Single JS file handles all formats at runtime. Format detection uses DOM inspection:
typeof Reveal !== "undefined"→ RevealJSdocument.body.classList.contains("quarto-dashboard")→ DashboardOtherwise → standard HTML
Key classes:
QuartoAxeChecker— Orchestrates scanning. Loads axe-core from CDN, runs scans, creates reporters.QuartoAxeDocumentReporter— Format-specific DOM report (overlay, slide, or offcanvas)QuartoAxeConsoleReporter— Logs violations to browser consoleQuartoAxeJsonReporter— Dumps full axe result as JSON to console
Format-Specific Behavior
HTML (standard)
Report: overlay appended to
<main>(or<body>)Interaction: hover highlights violation elements
Rescan: none (static page)
RevealJS
Report: dedicated
<section>slide appended to.reveal .slidesInteraction: click navigates to the slide containing the violation
Scan prep: temporarily removes
hidden/aria-hiddenfrom all slides so axe can inspect themRescan: none (slides are static)
Dashboard
Report: Bootstrap offcanvas sidebar with toggle button
Interaction: hover highlights violation elements
Rescan: triggered by
shown.bs.tab(page/card tabs),popstate(back/forward),bslib.sidebar(sidebar toggle)Generation counter prevents stale scan results from overwriting newer ones
Adding a New HTML Format
If a new HTML-based format is added that needs axe support:
TypeScript (
format-html.ts): Add format detection to the axe dependency injection logicSCSS: Create format-specific styles if the report placement differs. If the format uses Bootstrap themes, the CSS custom property bridge handles colors automatically.
JavaScript (
axe-check.js):Add format detection in
QuartoAxeDocumentReporter.report()— this determines whichcreateReport*()method is calledImplement a
createReport*()method for the format's DOM structureIf the format has dynamic content changes, add rescan triggers in
setupDashboardRescan()(or create a format-specific equivalent)
Tests: Add Playwright test fixtures (
.qmdfiles intests/docs/playwright/<format>/) and parameterized test cases inaxe-accessibility.spec.ts
Key Design Decisions
CDN loading
axe-core (~600KB) is loaded from cdn.skypack.dev at runtime rather than bundled. This keeps the Quarto distribution small since axe is a dev-only feature. Tradeoff: requires internet, fails silently in offline/CSP environments.
Options encoding
Options are base64-encoded in a <script id="quarto-axe-checker-options"> tag. This prevents JSON containing </script> from breaking HTML parsing. The runtime decodes with atob().
Generation counter (dashboard rescan)
Rapid tab switches can queue multiple axe.run() calls. The scanGeneration counter ensures only the latest scan's results are displayed. Earlier scans complete but their results are discarded if a newer scan was started.
Known Limitations
SCSS compilation: RevealJS sass-bundles compile separately from themes. CSS custom properties bridge this gap, but direct SCSS variable sharing isn't possible without pipeline changes.
CDN dependency: No offline fallback. See
quarto-cli-2u4ffor documentation task.Popstate delay: 50ms
setTimeoutfor dashboard back/forward navigation. Seequarto-cli-1fdffor improvement task.Multi-main elements:
createReportOverlay()usesdocument.querySelector('main')which returns the first match. Multiple<main>elements is invalid HTML, so this is acceptable.