Description
The interactive axe overlay (axe: output: document) is Quarto's tool for an author eyeballing accessibility while editing under quarto preview. It runs axe-core exactly once, at page load. But axe-core evaluates the rendered, visible DOM: color-contrast is computed from the colours on screen right now, and hidden elements (display: none) are skipped entirely. So two ordinary reader interactions invalidate the report, and the overlay gives no indication it's showing results for a state the page is no longer in:
- Toggling dark/light mode. With
theme: {light: ..., dark: ...}, the initial scan runs in whichever scheme loads first. Toggle the scheme and every color-contrast result is potentially wrong. A full page reload re-scans in the persisted scheme, but nothing tells the author a reload is needed.
- Expanding collapsed content. Violations inside collapsed callouts, folded code, closed tabsets, etc. are invisible to the load-time scan. This case is worse than stale: expanding doesn't trigger a rescan, and a reload re-collapses the content — so these violations are unreachable by the overlay no matter what the author does.
Dashboards already solve this: setupDashboardRescan re-runs axe and rebuilds the report on shown.bs.tab, popstate, and bslib.sidebar — but it's gated to body.quarto-dashboard:
- rescan machinery:
|
setupDashboardRescan() { |
|
// Page tabs and card tabsets both fire shown.bs.tab on the document |
|
document.addEventListener("shown.bs.tab", () => this.rescanDashboard()); |
|
|
|
// Browser back/forward — showPage() toggles classes without firing shown.bs.tab. |
|
// The 50ms delay lets the dashboard finish toggling .active classes on tab panes |
|
// before axe scans, so the correct page content is visible. |
|
window.addEventListener("popstate", () => { |
|
setTimeout(() => this.rescanDashboard(), 50); |
|
}); |
|
|
|
// bslib sidebar open/close — fires with bubbles:true after transition ends |
|
document.addEventListener("bslib.sidebar", () => this.rescanDashboard()); |
|
} |
- dashboard-only gate:
|
if (document.body.classList.contains("quarto-dashboard") && |
|
this.options.output === "document") { |
|
this.setupDashboardRescan(); |
|
} |
Proposal: extend the same rescan to regular HTML documents when output: document:
- rescan when the colour scheme toggles (the
.quarto-color-scheme-toggle click handler / toggleColorMode);
- rescan on Bootstrap state events, as dashboards do:
shown.bs.collapse / hidden.bs.collapse (callouts, code folding), shown.bs.tab (tabsets), plus shown.bs.dropdown / shown.bs.modal / shown.bs.offcanvas where relevant;
- and/or a manual "Rescan" button in the overlay as the catch-all, so the author can always refresh the report to match whatever state they've put the page in.
Most of the machinery exists; the work is ungating it and pointing the re-render at the overlay report element rather than the dashboard offcanvas.
This is deliberately scoped to the interactive overlay under quarto preview. Automated coverage of viewport/theme/interaction states (for output: json, CI, etc.) is a different problem I'll write up separately.
Steps to reproduce
---
title: "Axe overlay staleness repro"
format:
html:
theme:
light: flatly
dark: darkly
axe:
output: document
---
## Dark-mode-only contrast failure
<p style="color: #555555;">This paragraph is `#555555` — fine on flatly's white
background, but low contrast on darkly's dark background.</p>
## Collapsed callout hiding a violation
::: {.callout-note collapse="true" title="Expand me"}
<img src="figure.png" width="50" height="50">
The image above has no alt text — invisible to axe while collapsed.
:::
(figure.png is any image; it exists only to carry a missing alt.)
quarto preview the document (the overlay needs http:; module scripts don't load from file:).
- Note the overlay report at the bottom of the page.
- Click the dark-mode toggle (top right). The page turns dark; the overlay doesn't change.
- Expand the callout. The overlay doesn't change.
Actual behavior
The overlay shows the load-time scan forever. Verified by driving headless Chrome over CDP and comparing the overlay's contents against a fresh in-page axe.run() after each interaction (Quarto 1.10.12):
| State |
Overlay shows |
Fresh axe.run() finds |
| Load (light, collapsed) |
load-time results |
same as overlay |
| After dark toggle |
unchanged |
+ color-contrast (2 nodes: the #555555 paragraph and its inline code) |
| After expanding callout |
unchanged |
+ image-alt (the callout's image) |
Reloading the page in dark mode (the scheme persists via localStorage) does surface the color-contrast violations — confirming they're real and that a fresh scan catches them — but the callout re-collapses on reload, so the image-alt violation can never appear in the overlay.
Expected behavior
The overlay report reflects the page state the author is looking at: it rescans (automatically on scheme toggle and expand/collapse events, or at minimum via a manual "Rescan" button) so that mode-specific contrast failures and violations inside collapsible content are catchable during interactive checking, the way they already are in dashboards.
Your environment
- Quarto 1.10.12
- macOS 26.5.1 (build 25F80), Chrome (headless, CDP) for verification
AI-assisted investigation, grounded in the rendered output and the quarto-cli source at commit 38ef483 (per CONTRIBUTING.md "Using AI tools to investigate").
Description
The interactive axe overlay (
axe: output: document) is Quarto's tool for an author eyeballing accessibility while editing underquarto preview. It runs axe-core exactly once, at page load. But axe-core evaluates the rendered, visible DOM:color-contrastis computed from the colours on screen right now, and hidden elements (display: none) are skipped entirely. So two ordinary reader interactions invalidate the report, and the overlay gives no indication it's showing results for a state the page is no longer in:theme: {light: ..., dark: ...}, the initial scan runs in whichever scheme loads first. Toggle the scheme and everycolor-contrastresult is potentially wrong. A full page reload re-scans in the persisted scheme, but nothing tells the author a reload is needed.Dashboards already solve this:
setupDashboardRescanre-runs axe and rebuilds the report onshown.bs.tab,popstate, andbslib.sidebar— but it's gated tobody.quarto-dashboard:quarto-cli/src/resources/formats/html/axe/axe-check.js
Lines 331 to 344 in 38ef483
quarto-cli/src/resources/formats/html/axe/axe-check.js
Lines 397 to 400 in 38ef483
Proposal: extend the same rescan to regular HTML documents when
output: document:.quarto-color-scheme-toggleclick handler /toggleColorMode);shown.bs.collapse/hidden.bs.collapse(callouts, code folding),shown.bs.tab(tabsets), plusshown.bs.dropdown/shown.bs.modal/shown.bs.offcanvaswhere relevant;Most of the machinery exists; the work is ungating it and pointing the re-render at the overlay report element rather than the dashboard offcanvas.
This is deliberately scoped to the interactive overlay under
quarto preview. Automated coverage of viewport/theme/interaction states (foroutput: json, CI, etc.) is a different problem I'll write up separately.Steps to reproduce
(
figure.pngis any image; it exists only to carry a missingalt.)quarto previewthe document (the overlay needshttp:; module scripts don't load fromfile:).Actual behavior
The overlay shows the load-time scan forever. Verified by driving headless Chrome over CDP and comparing the overlay's contents against a fresh in-page
axe.run()after each interaction (Quarto 1.10.12):axe.run()findscolor-contrast(2 nodes: the#555555paragraph and its inlinecode)image-alt(the callout's image)Reloading the page in dark mode (the scheme persists via
localStorage) does surface thecolor-contrastviolations — confirming they're real and that a fresh scan catches them — but the callout re-collapses on reload, so theimage-altviolation can never appear in the overlay.Expected behavior
The overlay report reflects the page state the author is looking at: it rescans (automatically on scheme toggle and expand/collapse events, or at minimum via a manual "Rescan" button) so that mode-specific contrast failures and violations inside collapsible content are catchable during interactive checking, the way they already are in dashboards.
Your environment
AI-assisted investigation, grounded in the rendered output and the
quarto-clisource at commit 38ef483 (per CONTRIBUTING.md "Using AI tools to investigate").