Skip to content

Restore keyboard focus indicator on bare .btn buttons - #14785

Open
cwickham wants to merge 1 commit into
mainfrom
bare-btn-focus-indicator
Open

Restore keyboard focus indicator on bare .btn buttons#14785
cwickham wants to merge 1 commit into
mainfrom
bare-btn-focus-indicator

Conversation

@cwickham

@cwickham cwickham commented Aug 18, 2026

Copy link
Copy Markdown
Member

Description

Closes #14774.

Three Quarto buttons carry Bootstrap's btn class with no btn-* variant class: the code tools button, a website's sidebar toggle, and a website's sidebar search button. Bootstrap's .btn:focus-visible rule sets outline: 0 plus a box-shadow that only the variant classes define. On these buttons the box-shadow computes to none, so keyboard focus shows no indicator. This fails WCAG 2.2 SC 2.4.7 (Focus Visible).

This PR adds one rule that restores the browser's native focus ring on the three buttons with outline: revert.

Reasons to prefer the native ring over Bootstrap's box-shadow halo:

Site authors can still restyle the focus state, because user SCSS compiles after Quarto's rules. For the code tools button the new selector has the same specificity as Bootstrap's, so any override that worked before still works (verified in a rendered document). For the two nav buttons an override must match the new selector's specificity, for example by including .quarto-secondary-nav.

No documentation change is needed: the fix adds no option and changes no documented behavior.

Tests

The new spec html-focus-indicator-bare-btn.spec.ts covers all three buttons in Chromium, Firefox, and WebKit. It moves focus with real Tab presses (Option+Tab on WebKit, which ships Safari's default keyboard-navigation settings), then asserts that the computed outline-style is not none. Real key presses are necessary because programmatic focus does not match :focus-visible. All nine tests fail on a build without the fix and pass with it. The fixtures are a standalone code-tools document and a minimal website with a collapsed sidebar.

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, see above
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone of quarto-cli
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

The code tools button, website sidebar toggle, and website sidebar
search button carry Bootstrap's btn class with no btn-* variant class.
Bootstrap's .btn:focus-visible sets outline: 0 and substitutes a
box-shadow that only the variant classes define, so these buttons took
keyboard focus with no visible indicator, failing WCAG 2.4.7.

Restore the browser's native focus ring on them with outline: revert.
An outline also survives forced-colors mode, where box-shadow computes
to none, and matches the code copy button's indicator (#12118).
@posit-snyk-bot

posit-snyk-bot commented Aug 18, 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.

@cwickham
cwickham marked this pull request as ready for review August 20, 2026 15:06
@cwickham
cwickham requested a review from cderv August 20, 2026 15:07

@cderv cderv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and for the very thorough issue that goes with it. I checked the claims in the description against the compiled CSS and a real browser rather than taking them on trust, and the diagnosis holds up in full: with the rule commented out the three buttons compute outline-style: none, box-shadow: none, and an empty --bs-btn-focus-shadow-rgb. I also ran the new spec locally in all three engines: nine pass with the fix and nine fail without it, so the assertion is doing real work.

One substantive request, on the .quarto-secondary-nav prefix, plus two small things. Details inline.

The rest checks out.

// Restore the browser's ring; unlike box-shadow it survives forced colors.
// See https://github.com/quarto-dev/quarto-cli/issues/14774
.quarto-secondary-nav .quarto-btn-toggle:focus-visible,
.quarto-secondary-nav .quarto-search-button:focus-visible,

@cderv cderv Aug 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we drop the .quarto-secondary-nav prefix from these two selectors? It is the only reason the nav buttons end up with a different override story from the code tools button, and it does not appear to buy anything.

I tested this. Rendering a site with a user SCSS rule setting outline: 3px dashed red on all three selectors, the code tools button picks up the override (same specificity, user rules come later) while the two nav buttons keep the user-agent ring. With the prefix removed and nothing else changed, the override wins on all three. And without any user rule, the unprefixed selectors still beat Bootstrap: outline-style computes to auto on both nav buttons, because Quarto's rules layer already comes after the framework layer, so equal specificity plus later source order is enough.

Scoping is not lost either I think. Both buttons are emitted in exactly one place, nav-before-body.ejs, and only ever inside .quarto-secondary-nav.

That would leave all three buttons overridable the same way, and the description would no longer need the caveat about authors having to match the new specificity.

Did you check about this maybe ?

@@ -0,0 +1,19 @@
project:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture needs a .gitignore, otherwise rendering it leaves an untracked _site/ behind (plus the .gitignore Quarto generates for .quarto/). The driver calls cleanoutput() without projectOutDir, so it cannot clean a project render — which is why the neighbouring website fixture ships one:

https://github.com/quarto-dev/quarto-cli/blob/456e129742a31cb10298dea2c3fb693dd0c0504d/tests/docs/playwright/website/issue-14667/.gitignore

/.quarto/
/_site/

target: Locator,
maxTabs = 25,
): Promise<boolean> {
const tabKey = browserName === "webkit" ? "Alt+Tab" : "Tab";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a change request, just a note for the record: on the Windows WebKit build the three tests also pass with a plain Tab, so the Safari tab-order behaviour the comment describes does not reproduce there. Alt+Tab passes as well and CI runs on Linux, so I would leave this as is — worth knowing if it ever gets flaky.

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.

a11y: buttons with no Bootstrap variant class have no visible keyboard focus indicator

3 participants