Skip to content

Make the collapsible callout header a keyboard-accessible button - #14981

Open
cwickham wants to merge 10 commits into
mainfrom
fix/4934-callout-toggle-button
Open

cwickham wants to merge 10 commits into
mainfrom
fix/4934-callout-toggle-button

Conversation

@cwickham

@cwickham cwickham commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Description

Fixes #4934. Fixes #15177. The header of a collapsible callout was a <div> with aria-expanded, no role, and no tab stop. Its aria-label="Toggle callout" also hid the title from screen readers.

The header now wraps the icon, title, and chevron in a native <button> (the APG disclosure pattern):

<div class="callout-header callout-collapsible d-flex align-content-center">
  <button type="button" class="callout-toggle-btn d-flex flex-fill collapsed"
          data-bs-toggle="collapse" data-bs-target=".callout-1-contents"
          aria-controls="callout-1" aria-expanded="false">
    <span class="callout-icon-container">…</span>
    <span class="callout-title-container flex-fill"><span class="screen-reader-only">Note </span>Title</span>
    <span class="callout-btn-toggle …"><i class="callout-toggle"></i></span>
  </button>
</div>
  • Keyboard users can reach the toggle with Tab and operate it with Enter or Space.
  • The name of the button is the callout type and the title, for example "Note Title".
  • The button target stays the .callout-N-contents class. Margin content that moves out of the callout keeps this class, so it still collapses with the callout.
  • The hidden type label now ends with a space. Without it, the button name was "NoteTitle". This applies to every titled callout, static ones too, so it also fixes the joined word in the page text and the website search index (Callout type label runs into the title in the HTML text (NoteRemember) #15177).
  • Bootstrap updates collapsed on the trigger only. Thus the corner-radius rule now uses :has(> .callout-toggle-btn.collapsed) instead of a class on the header.

No visual change. I rendered 48 callouts with main and with this branch: all types, the default, simple, and minimal appearances, collapsed and expanded, static callouts, and margin content. All screenshots are pixel-identical in Chromium and WebKit, before and after a toggle. The only new visual is the focus ring on keyboard focus. With forced colors (Chromium emulation), the header looks the same as on main, and the focus ring uses the system highlight color.

Manual check: on a test page with every appearance, margin content, and links in the title, body and margin note, I tabbed to each collapsible header and opened and closed it with Enter and Space. Links in the body and margin note work when the callout is open and leave the Tab order when it is collapsed. VoiceOver announces each header as a button with the callout type, title and expanded state.

axe: the aria-allowed-attr finding (35 instances on the test page) is gone.

Links in a collapsible title: a link, cross-reference, or footnote marker in the title is now inside the button, so axe reports nested-interactive. This PR does not change how these links behave: in Chromium and WebKit, a click on a link or cross-reference already toggles the callout and does not follow the link. We could add a note to the callout docs that collapsible titles should not contain links.

Custom CSS: selectors for collapsible callouts change. .callout-header[aria-expanded] and .callout-header.collapsed become .callout-toggle-btn[aria-expanded] and .callout-toggle-btn.collapsed. The icon and title containers are now <span>, so div.callout-title-container no longer matches collapsible callouts. A GitHub code search found no hand-written CSS that uses the old header selectors. The one script it found adds role="button" to the header, which is a workaround for this issue.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR — not needed: authors write collapsible callouts the same way as before.

Tests: a smoke-all test for the markup, including margin content in a collapsible callout (callouts/a11y/issue-4934.qmd) and a Playwright keyboard test (html-callouts.spec.ts). The existing callout tests pass locally. I did not run the full suite locally; CI covers it.

AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

The collapse toggle was a div with aria-expanded, no role, no tab stop and
an aria-label that hid the title. The header now wraps its icon, title and
chevron in a <button> (APG disclosure pattern), so it is reachable with Tab,
operable with Enter/Space, and named by the callout type and title.

Rendering is pixel-identical to main in Chromium and WebKit for default,
simple and minimal callouts, collapsed and expanded, apart from the new
focus ring.

Fixes #4934
@posit-snyk-bot

posit-snyk-bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…llm-docs

The collapsed state now lives on the toggle button, not the header (#4934).
The non-collapsible header was built and then overwritten by the
collapsible path. Fill the header in exactly one place for each case.
Remove only the UA button styles (appearance, background, border,
padding, font, color, text alignment, letter spacing). Display, cursor
and box-sizing no longer need to be restored, and forced-colors
handling stays the browser default.
…le-button

# Conflicts:
#	news/changelog-1.11.md
@cwickham
cwickham marked this pull request as ready for review October 23, 2026 17:15
@cwickham
cwickham requested a review from cderv October 23, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Callout type label runs into the title in the HTML text (NoteRemember) Collapsed callouts using invalid ARIA attribute (a11y issue)

2 participants