You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Starlight mobile menu leaves focusable button aria-expanded stale #160
The mobile menu's focusable button should expose aria-expanded="true" when its controlled sidebar opens, and false when it closes.
Observed behavior
During docs PR #157 browser verification with @astrojs/starlight 0.41.11, clicking the default mobile menu opens the sidebar and makes .main-frame inert. The outer <starlight-menu-button> changes to aria-expanded="true"; its child <button aria-label="Menu" aria-controls="starlight__sidebar"> retains aria-expanded="false".
The visible menu operates, but assistive technology querying the focused button receives the wrong expanded state. This is independent of PR #157's Hero and SiteTitle overrides; that PR does not override Header, MobileMenuToggle or Sidebar.
Root-cause analysis
Mechanism: upstream components/MobileMenuToggle.astro initializes aria-expanded="false" on button. StarlightMenuButton.setExpanded() calls this.setAttribute('aria-expanded', String(expanded)) on the custom element, not this.btn. CSS reads the wrapper state, so visual checks pass while the accessible button stays stale.
Missed control: default-component behavior was trusted; build and visual smoke tests do not compare the focusable button's accessibility state with the rendered sidebar.
Blast radius: mobile menu screen-reader state on every docs route using this Starlight version. Search, theme, menu focus trap and branding are separate.
Root fix: correct the upstream component to update the actual button state alongside its wrapper/CSS state; adopt the fixed release and add a behavioral assertion that the focused button's expanded state tracks the menu open/close cycle. Avoid copying the whole upstream component into this site.
Evidence and classification
Installed upstream source: node_modules/@astrojs/starlight/components/MobileMenuToggle.astro in the PR #157 test worktree. Chromium 154 CDP check observed wrapper expanded: true, main inert: true, sidebar visibility: visible, while the inner button still reported false.
Separable, non-blocking finding: the changed branding uses the unchanged default menu. Track the upstream correction and dependency adoption here; PR #157 will list this issue under Filed issues.
STATUS — the fix is upstream's, the adoption is docs#161, and the defect's mechanism is gone from the built output.
Agent: dsh-opencharly
What the issue asked for, and what the measurement found
The issue's own root fix was: "correct the upstream component … adopt the fixed release and add a behavioural [test]".I checked whether the fixed release already exists, and it does — as a REWRITE rather than a patch:
# the published package, 0.42.6, packages/@astrojs/starlight/components/MobileMenuToggle.astro:5
** <button popovertarget="starlight__sidebar" class="sl-menu-button sl-flex md:sl-hidden print:hidden"> **
and: ** no `aria-expanded` attribute anywhere in the file **
In 0.41.x the button carried aria-expanded="false" and StarlightMenuButton.setExpanded() updated the wrapper — the mechanism the issue's RCA names. In 0.42.x the button is a native popover invoker, so the browser owns the expanded state and nothing manual can go stale.The defect CLASS is removed, not patched.
And the pin was excluding it:docs/package.json had ^0.41.6, and for a 0.x version a caret pins the MINOR — so 0.42.x was never installable from that range.The bump is what makes the fix reachable.
What landed
opencharly/docs#161 — "fix(deps): bump @astrojs/starlight to 0.42.6, which removes the stale mobile-menu aria-expanded"
package.json (+1/-1) "^0.41.6" -> "^0.42.6"
package-lock.json (+617/-1927) resolved to 0.42.6 by npm install --package-lock-only
Gates, on the bumped tree — the same npm ci + production build the docs-site candy runs, which is this surface's own gate and the failure mode its skill names:
$ npm ci --no-audit --no-fund added 301 packages in 4s rc=0
$ npm run build [starlight:pagefind] Found 1503 HTML files.
[build] ** 1503 page(s) built in 1m 2s ** Complete! rc=0
And the emitted artefact, verified at its actual boundary rather than in the source:
One nuance about the acceptance criterion, stated rather than glossed
The issue words the desired behaviour as aria-expanded="true"/"false".The fixed release satisfies the intent — the focused button no longer reports a stale expanded state — by different means: the attribute is absent by design, because a native popover invoker's state comes from the popover itself (:popover-open), which is what assistive technology reads.So a behavioural test for this should assert "the button is a popover invoker and no stale aria-expanded is emitted" — which is what the greps above check — rather than "aria-expanded flips to true on click".Recording the difference so the assertion written later matches the fixed behaviour instead of the broken one's shape.
The consumer step, named
docs-site (the candy in charly) pins DOCS_REF to an immutable docs commit, and the pin is TWO occurrences — DOCS_REF and the literal in the docs-site-pinned-commit check's contains: matcher.task docs:pin fails until both name this repo's new main head, so the sequence is: docs#161 merges → charly re-pins both → the check-docs bed proves the bumped site.That step cannot begin until this lands, which is why it is named here rather than promised.
Nothing further from me on this thread until the PR has a verdict.
Agent: dsh-opencharly Assisted-by: dsh deepseek-v4.1-flash (fully tested and validated)
STATUS — #160 — slug umbrella-prose-truth — this issue is closed by #187, which carries the fix the same dependency update brings.
The mechanism, and where it ends. The mobile-menu button carried aria-expanded="false", and StarlightMenuButton.setExpanded() updated the wrapper rather than the button, so assistive technology read a stale expanded state on every docs route. The 0.42 line rewrites that button onto the native Popover API (popovertarget="starlight__sidebar"), so the browser owns the expanded state and it cannot fall out of sync — the manual attribute is gone rather than kept in step.
The proof, on the successor's tree: the built site carries the popover invoker on 1698 pages and aria-expanded on none; the base build, built with the same commands, carries aria-expanded="false" on 1698 pages and the invoker on none. The greps are pasted in the successor's body.
The other issue this successor closes:#159 (GHSA-rj75-hqrm-r3gf), fixed through the upstream release that issue's acceptance asks for.
It supersedes#161, whose branch is conflicting against main. This issue is closed when the successor lands.
Agent: umbrella-prose-truth Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system)
RESOLVED — #160 — the stale aria-expanded is gone from the built output, and the fix is landed.
What landed.#187 — squash b5cb81050283e0c130029ee8abd73f74ca572351, tag v2026.299.0712, changelog CHANGELOG/2026.299.0712.md. The Starlight 0.42 line rewrites the mobile-menu button onto the native Popover API (popovertarget="starlight__sidebar") and drops the manual aria-expanded attribute, so the browser owns the expanded state and it cannot fall out of sync with the wrapper.
The evidence, in the artefact that is actually shipped. The built site of the landed tree carries the popover invoker on 1698 pages and aria-expanded on none; the base build carries aria-expanded="false" on 1698 pages and the invoker on none. The <starlight-menu-button> custom element and its toggleExpanded() script are gone from all 1698 pages, and <sl-sidebar-pane popover> is on all 1698. The 1699th page is 404.html, which carries no sidebar.
The mechanism this issue named is gone.StarlightMenuButton.setExpanded() updated the wrapper and not the button; the manual attribute is now not maintained by anything, because nothing maintains it — the popover's expanded state belongs to the browser.
Why this issue stayed open past the merge. The merge was performed by github-actions[bot], and a bot merge does not close a Closes #N line — so the issue is closed here, by hand, on the merged commit.
What is still open. The dev tree's undici advisories through wrangler → miniflare — #163, claimed and in flight.
Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
Activity
aitrawog commentedon Oct 16, 2026
STATUS — the fix is upstream's, the adoption is
docs#161, and the defect's mechanism is gone from the built output.Agent: dsh-opencharly
What the issue asked for, and what the measurement found
The issue's own root fix was: "correct the upstream component … adopt the fixed release and add a behavioural [test]". I checked whether the fixed release already exists, and it does — as a REWRITE rather than a patch:
In
0.41.xthe button carriedaria-expanded="false"andStarlightMenuButton.setExpanded()updated the wrapper — the mechanism the issue's RCA names. In0.42.xthe button is a native popover invoker, so the browser owns the expanded state and nothing manual can go stale. The defect CLASS is removed, not patched.And the pin was excluding it:
docs/package.jsonhad^0.41.6, and for a0.xversion a caret pins the MINOR — so0.42.xwas never installable from that range. The bump is what makes the fix reachable.What landed
opencharly/docs#161— "fix(deps): bump@astrojs/starlightto 0.42.6, which removes the stale mobile-menuaria-expanded"Gates, on the bumped tree — the same
npm ci+ production build thedocs-sitecandy runs, which is this surface's own gate and the failure mode its skill names:And the emitted artefact, verified at its actual boundary rather than in the source:
One nuance about the acceptance criterion, stated rather than glossed
The issue words the desired behaviour as
aria-expanded="true"/"false". The fixed release satisfies the intent — the focused button no longer reports a stale expanded state — by different means: the attribute is absent by design, because a native popover invoker's state comes from the popover itself (:popover-open), which is what assistive technology reads. So a behavioural test for this should assert "the button is a popover invoker and no stalearia-expandedis emitted" — which is what the greps above check — rather than "aria-expandedflips to true on click". Recording the difference so the assertion written later matches the fixed behaviour instead of the broken one's shape.The consumer step, named
docs-site(the candy incharly) pinsDOCS_REFto an immutable docs commit, and the pin is TWO occurrences —DOCS_REFand the literal in thedocs-site-pinned-commitcheck'scontains:matcher.task docs:pinfails until both name this repo's newmainhead, so the sequence is:docs#161merges →charlyre-pins both → thecheck-docsbed proves the bumped site. That step cannot begin until this lands, which is why it is named here rather than promised.Nothing further from me on this thread until the PR has a verdict.
Agent: dsh-opencharly
Assisted-by: dsh deepseek-v4.1-flash (fully tested and validated)
aitrawog commentedon Oct 26, 2026
STATUS — #160 — slug
umbrella-prose-truth— this issue is closed by #187, which carries the fix the same dependency update brings.The mechanism, and where it ends. The mobile-menu button carried
aria-expanded="false", andStarlightMenuButton.setExpanded()updated the wrapper rather than the button, so assistive technology read a stale expanded state on every docs route. The 0.42 line rewrites that button onto the native Popover API (popovertarget="starlight__sidebar"), so the browser owns the expanded state and it cannot fall out of sync — the manual attribute is gone rather than kept in step.The proof, on the successor's tree: the built site carries the popover invoker on 1698 pages and
aria-expandedon none; the base build, built with the same commands, carriesaria-expanded="false"on 1698 pages and the invoker on none. The greps are pasted in the successor's body.The other issue this successor closes: #159 (GHSA-rj75-hqrm-r3gf), fixed through the upstream release that issue's acceptance asks for.
It supersedes #161, whose branch is conflicting against
main. This issue is closed when the successor lands.Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system)
fix(deps): adopt the Starlight 0.42 line and the upstream selector-pa…
aitrawog commentedon Oct 26, 2026
RESOLVED — #160 — the stale
aria-expandedis gone from the built output, and the fix is landed.What landed. #187 — squash
b5cb81050283e0c130029ee8abd73f74ca572351, tagv2026.299.0712, changelogCHANGELOG/2026.299.0712.md. The Starlight 0.42 line rewrites the mobile-menu button onto the native Popover API (popovertarget="starlight__sidebar") and drops the manualaria-expandedattribute, so the browser owns the expanded state and it cannot fall out of sync with the wrapper.The evidence, in the artefact that is actually shipped. The built site of the landed tree carries the popover invoker on 1698 pages and
aria-expandedon none; the base build carriesaria-expanded="false"on 1698 pages and the invoker on none. The<starlight-menu-button>custom element and itstoggleExpanded()script are gone from all 1698 pages, and<sl-sidebar-pane popover>is on all 1698. The 1699th page is404.html, which carries no sidebar.The mechanism this issue named is gone.
StarlightMenuButton.setExpanded()updated the wrapper and not the button; the manual attribute is now not maintained by anything, because nothing maintains it — the popover's expanded state belongs to the browser.Why this issue stayed open past the merge. The merge was performed by
github-actions[bot], and a bot merge does not close aCloses #Nline — so the issue is closed here, by hand, on the merged commit.What is still open. The dev tree's
undiciadvisories throughwrangler→miniflare— #163, claimed and in flight.Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)