Skip to content

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

Merged
cderv merged 4 commits into
mainfrom
bare-btn-focus-indicator
Sep 2, 2026
Merged

Restore keyboard focus indicator on bare .btn buttons#14785
cderv merged 4 commits 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. Each of the three selectors is a single class, the same specificity as Bootstrap's own rule. A plain override therefore wins on all three buttons. For example, .quarto-search-button:focus-visible { outline: 3px dashed red; } replaces the ring on the sidebar search button. I confirmed this in a rendered site in Chromium, Firefox, and WebKit.

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.

Comment thread src/resources/formats/html/bootstrap/_bootstrap-rules.scss Outdated
Comment thread tests/docs/playwright/website/bare-btn-focus/_quarto.yml
Drop the `.quarto-secondary-nav` prefix from the two nav button
selectors. Both buttons are emitted in exactly one place,
nav-before-body.ejs, and only ever inside `.quarto-secondary-nav`, so
the prefix bought no scoping. It did make the two nav buttons harder to
override than the code tools button: a site author's rule at single-class
specificity won on the code tools button but lost on the nav buttons.
All three now behave the same.

Add a .gitignore to the bare-btn-focus fixture so a project render does
not leave `_site/` and `.quarto/` untracked. The driver calls
cleanoutput() without projectOutDir, so it cannot clean a project render.
@cwickham

cwickham commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Thanks @cderv — both changes are in, as 92a59ef.

Prefix dropped. quarto-btn-toggle and quarto-search-button each appear in exactly one template, nav-before-body.ejs L66 and L86, both inside <nav class="quarto-secondary-nav">. Nothing else in src/ emits them, so the prefix bought no scoping.

I rendered the same site twice, once per selector variant, with a user SCSS rule setting outline: 3px dashed red, and tabbed to each button in all three engines:

code tools sidebar toggle sidebar search
prefixed dashed 3px red auto (UA ring) auto (UA ring)
unprefixed dashed 3px red dashed 3px red dashed 3px red

Identical in Chromium, Firefox, and WebKit. Your finding reproduces exactly.

I also checked the no-user-rule case against the fixtures: all nine button/engine combinations compute outline-style: auto, so equal specificity plus later source order is enough, as you said. The spec passes 9/9 on the unprefixed build.

The specificity caveat is out of the PR description.

Fixture .gitignore added.

@cwickham
cwickham requested a review from cderv September 1, 2026 22:24

@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.

LGTM, thanks;

🤖 went through the diff again after and flagged something worth a mention. #14826, already on main, added its own focus-visible rule for the new sidebar-item-toggle buttons:

.sidebar-item .sidebar-item-toggle:focus-visible,
.sidebar-item button.sidebar-item-text:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}

That one uses outline: 2px solid currentColor with outline-offset: 2px, while this PR uses outline: revert. Same problem, same sidebar region, two different conventions now living next to each other.

Not blocking, both fix the underlying issue and neither is wrong. Do you think we need a follow-up issue to converge on one approach ?

I am merging this anyway, but I figured it was good to mention in case we want common ways to fix axe problems.

@cderv
cderv merged commit d742b90 into main Sep 2, 2026
51 checks passed
@cderv
cderv deleted the bare-btn-focus-indicator branch September 2, 2026 10:09
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