Make website sidebar section toggles keyboard-operable buttons - #14826
Open
cwickham wants to merge 2 commits into
Open
Make website sidebar section toggles keyboard-operable buttons#14826cwickham wants to merge 2 commits into
cwickham wants to merge 2 commits into
Conversation
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. |
cwickham
force-pushed
the
fix/sidebar-disclosure
branch
from
August 28, 2026 16:33
ccdcb89 to
5c710d6
Compare
The expandable sidebar sections were <a> elements with no href, so the keyboard could not reach them at all, and they carried role="navigation", which made each one an unnamed nested navigation landmark instead of a named control. Sections without their own page now render one <button> that holds both the label and the chevron, so there is a single tabstop. Sections that do have a page keep their link and get a <button> chevron beside it, named after the section. Every disclosed list is named with aria-labelledby pointing at its own section label. Also removes the duplicated `role` attribute from the mobile sidebar toggle in nav-before-body.ejs, which was invalid HTML. Fixes #12116 Fixes #4935
cwickham
force-pushed
the
fix/sidebar-disclosure
branch
from
August 28, 2026 16:47
5c710d6 to
216b120
Compare
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.
Description
Fixes #12116
Fixes #4935
The expandable sections in the website left sidebar were
<a>elements with nohref, so the keyboard could not reach them (#12116). They also carriedrole="navigation", which made each section an unnamed nested navigationlandmark rather than a named control, and split the label and the chevron into
two tabstops (#4935).
Relationship to #14813
#14813 gives every real
<nav>landmark a distinct label and closes #14376. Its"Out of scope" note defers the invalid
role="navigation"on the secondary-navand sidebar section toggles; this PR removes both. The two are complementary:
#14813 names the genuine landmarks, this PR deletes the fake ones.
They overlap in two files.
nav-before-body.ejsauto-merges — #14813 edits the<nav>open tags, this PR edits the<a>toggles below them.news/changelog-1.11.mdconflicts, because both add an entry to
## Accessibility; keep both entries.What changed
A section without its own page now renders one
<button>holding both thelabel and the chevron — one tabstop, the
buttonrole, and a name from the labeltext:
A section that is also a page keeps its link and gets a
<button>chevronbeside it, named
aria-label="Toggle section: <title>".Every disclosed list now carries
aria-labelledbypointing at its own sectionlabel, so a screen reader says which section the list belongs to and how many
items it holds.
Why a section with a page keeps two tabstops
The guidance on #4935 is one control with one tabstop. That works for a section
with no page of its own, and this PR does exactly that.
It cannot work for a section that is also a page: the title navigates and the
chevron expands, and one activation cannot do both. Merging them would mean a
user can no longer expand a section without leaving the current page — a
behavior change for every existing site that writes
section: page.qmd. So thelink stays and the chevron becomes a real
<button>, named per section so it isdistinct from the other chevrons. The name reuses the existing translatable
toggle-sectionkey, so all 34 shipped translations work with no new string.The
role="navigation"finding has changed#4935 calls the ARIA invalid and cites
aria-allowed-attrfrom a 2023 axe run.Where that stands today:
navigationunder Inherits into Roles foraria-expanded, via the abstractsectionrole. Valid then.sectionno longer supportsaria-expanded, and boththe Used in Roles and Inherits into Roles lists for
aria-expandedexcludenavigation. The premise of Left-nav menu expand/collapse should be buttons/use disclosure pattern (a11y issue) #4935 holds.navigation: { allowedAttrs: ['aria-expanded'] }. That silence is an axe policychoice, not evidence the markup is valid.
The worse problem was a different one:
navigationtakes its name from theauthor only, so the visible text "Group" was never the control's accessible name.
Chrome computed it as
navigation "".Removing
role="navigation"also deletes the spurious landmark each sectionadded, which is one source of the
landmark-uniqueviolation axe did report.It is not the only source — see the note on #14813 below.
Verification
Measured on a site covering both shapes, two depths, and both collapse states at
a 1440px viewport. Chrome's computed accessibility tree:
navigation ""button "Group" expanded=falsenavigation "Toggle section"button "Toggle section: landing" expanded=falselist "Group"EnterandSpaceboth toggle, andaria-expandedtracks the real state.the expanded sidebar state, at 1440px and 800px: 0 violations. Scanning the
expanded state matters, because a collapsed section is
display: noneand itsdisclosed list is never evaluated. On a website page
landmark-uniquegoesfrom 1 to 0. On a book page it goes from 2 nodes to 1: books emit further
unnamed
<nav>landmarks (#TOC,.page-navigation), which Give every website and booknavlandmark a distinct, localizable label #14813 fixes.Note that axe is not the detector that matters here — it says nothing about
keyboard operability, and saw nothing at all until the scan ran at 992px or
wider, because the sidebar is
display: nonebelow that width.their section name and expanded state, and keyboard navigation is usable.
default,cosmo,sketchy,darkly,vapor,litera,journal,minty). The merged buttoncomputes identically to the
<a>it replaced on 21 of 22 properties, includingfont, color, background, border and padding; the exception is
display: block→
flex, which is the point. Row heights, row positions and the chevron'sright edge are unchanged. That is why the button uses
align-items: baselinerather than
center—centerrenders 1-2px shorter per section, which shiftsevery row below it. (The section label computes body-grey rather than
link-blue in both old and new: the old
<a>had nohref, so link stylingnever applied to it.)
(
li.sidebar-item a.active—activeonly ever lands on a real link), themobile sidebar below 992px, and
collapse-levelplus the auto-expanded activesection at depth 1 and 2, in both websites and books.
New smoke tests in
tests/docs/smoke-all/website/sidebar-disclosure/cover bothshapes and both collapse states, and were confirmed to fail against the old
markup. The existing
tests/smoke/sitesuite passes (34 tests).Also in this PR
nav-before-body.ejscarriedroletwice on one element(
role="navigation" ... role="link"), which is invalid HTML. Both are removed,with the ARIA that duplicated the real toggle button beside it. That element is a
decorative mouse target; the adjacent
button.quarto-btn-toggleis thekeyboard-operable control and already works. Making it a button, or removing it
as redundant, needs a behavior decision and is left out.
Checklist
I have (if applicable):
AI-assisted PR