Make dark/light mode toggle an accessible switch - #14739
Draft
cwickham wants to merge 4 commits into
Draft
Conversation
The toggle was an <a href=""> with a CSS-drawn icon: no accessible name (axe link-name, WCAG 2.4.4/4.1.2), link role for a button-like control (Space did not activate it), and mode state conveyed only by a background-image swap that never reaches the accessibility tree. Now a <button type="button" role="switch" aria-checked> with the localized toggle-dark-mode string as aria-label, in all three places the control is created: website navbar/sidebar (navdarktoggle.ejs), plain-document fallback (quarto-html-after-body.ejs), and multi-page dashboards (format-dashboard-page.ts). setColorSchemeToggle() keeps aria-checked in sync alongside the existing .alternate class.
Positive: the toggle renders as button[role=switch][aria-checked] with a localized aria-label on websites (static), multi-page dashboards (static), and plain documents (script-text regex, since the fallback toggle is injected at DOMContentLoaded). Guard rails: light-only documents still emit no toggle markup at all, and the reader-mode toggle and navbar tool links keep their existing anchor shape. All fail against the pre-fix source except the light-only guard, which passes on both (verified by stashing the fix).
Collaborator
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
The toggle is now a <button role="switch"> instead of an <a>, so locators using a.quarto-color-scheme-toggle no longer match. Verified locally: all 36 tests across the five affected specs pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #13463.
Description
The dark/light mode toggle was an
<a href="">with a CSS-drawn icon, which fails all three prongs of WCAG 4.1.2 Name, Role, Value: no accessible name (the axelink-nameviolation reported in #13463), a link role for a button-like control (so Space doesn't activate it), and mode state conveyed only by abackground-imageswap that never reaches the accessibility tree — a screen-reader user gets no feedback on which mode is active, before or after toggling.This PR makes the toggle a WAI-ARIA APG switch —
<button type="button" role="switch" aria-checked>with the existing localizedtoggle-dark-modestring asaria-label— in all three places the control is created:navdarktoggle.ejs; keeps its existingtitletooltip)quarto-html-after-body.ejs)format-dashboard-page.ts, which now receivesformat.language)Why
<button>rather than ARIA on the existing<a>:role="switch"is permitted ona[href], but ARIA only changes what's announced, not how the element behaves: Space would still scroll the page instead of activating (links respond to Enter only), the emptyhrefwould still navigate/reload if JS hasn't bound yet, and link affordances (middle-click, "Open in New Tab", status-bar URL) would still apply. Making it a real<button>gets correct focus and Enter+Space activation natively, at the one-time cost of a selector change (the five Playwright specs updated here; custom CSS targetinga.quarto-color-scheme-toggleshould now target thebutton).setColorSchemeToggle()keepsaria-checkedin sync alongside the existing.alternateclass, so screen readers announce "Toggle dark mode, switch, off/on" and announce the state change on activation. A small SCSS reset (button.quarto-color-scheme-toggle) neutralizes UA button styling; all existing toggle styling is class-based, so visuals are unchanged (verified by screenshot in navbar, dashboard navbar, and floating placements).Behavior checked as unchanged: light-only documents still emit no toggle at all; the reader-mode toggle and navbar tool links keep their existing markup; the theme-switching JS itself is untouched apart from the one
aria-checkedline.Verified with axe-core (the
link-nameviolation clears; no new violations) and the Chrome accessibility tree (role=switch, name, andcheckedround-tripping on activation) for all three creation paths. Also verified with a manual VoiceOver pass across all placements — website navbar, website sidebar, dashboard navbar, and the floating fallback — plus the light-only control document (no toggle announced).Dark-by-default behavior verified with VoiceOver: the toggle announces the correct on/off state on load and after activation for author-dark-default (
theme:withdark:listed first), forrespect-user-color-scheme: trueunder both OS appearance settings, and when a previously chosen scheme is restored on reload.No documentation update needed: the quarto.org docs describe the toggle at the author-facing level (placement and behavior, both unchanged) and never reference its markup.
Checklist
I have (if applicable):
AI-assisted PR