Skip to content

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

Description

@cwickham

I have

  • searched the existing issues, and did not find a duplicate.
  • tried the latest release of Quarto.
  • made a minimal reproducible example.

Bug description

Quarto emits buttons that carry the btn class with no btn-* variant class. Such a button takes keyboard focus, matches :focus-visible, and shows no focus indicator at all. I found three:

Button Appears when
button.code-tools-button code-tools is on, at any viewport width
button.quarto-btn-toggle a website sidebar collapses (below 992px)
button.quarto-search-button a website has sidebar search, below 992px

This fails WCAG 2.2 Success Criterion 2.4.7 Focus Visible (Level AA): "Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible." axe-core has no rule for a missing focus indicator, so a clean axe report does not cover this.

Cause

Bootstrap removes the native ring and substitutes its own (_buttons.scss#L69-L79):

&:focus-visible {
  outline: 0;
  box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}

--bs-btn-focus-box-shadow references --bs-btn-focus-shadow-rgb (#L21). Only the btn-* variant classes define that property. On a bare .btn the reference is invalid, so box-shadow computes to none, and outline: 0 has already removed the native ring.

Bootstrap documents this behavior. Its Base class section says that by default ".btn controls have a transparent border and background color, and lack any explicit focus and hover styles", and that the class "is intended to be used in conjunction with our button variants, or to serve as a basis for your own custom styles". It then gives this instruction:

If you are using the .btn class on its own, remember to at least define some explicit :focus and/or :focus-visible styles.

Quarto uses .btn on its own in the places below, and defines no such styles for them.

The emission sites I found:

Quarto's .btn:focus:not(:focus-visible) rule is not involved. It is scoped to mouse input and does not apply to keyboard focus.

The other Quarto buttons I checked are unaffected, because they either carry a variant class (btn-primary, btn-dark, btn-quarto) or are not a .btn at all (code-copy-button, callout-btn-toggle). .btn-close is a separate component with a working focus shadow. I did not audit bslib components, dashboards, or revealjs, so there may be more.

Related: #12118 fixed the same symptom on the code copy button, which is not a .btn and had a different cause. #26 is the origin of the mouse-versus-keyboard rule above.

Steps to reproduce

  1. Save the document below as index.qmd.
  2. Run quarto render index.qmd.
  3. Open index.html, and press Tab.

The first tab stop is the "Code" button, at the top right. It takes focus with no indicator.

---
title: "Focus indicator test"
format:
  html:
    code-tools: true
    theme: cosmo
---

Press Tab. Focus moves to the Code button, which shows no indicator.

```r
1 + 1
```

For the two navigation buttons, run quarto create project website focus-test, replace _quarto.yml with the file below, render, then narrow the window under 992px and press Tab.

project:
  type: website

website:
  title: "focus-test"
  search:
    location: sidebar
  navbar:
    left:
      - href: index.qmd
        text: Home
  sidebar:
    style: floating
    search: true
    contents:
      - index.qmd
      - about.qmd

format:
  html:
    theme: cosmo

Actual behavior

All three buttons take focus with no indicator. Computed outline is none 0px and computed box-shadow is none, in light and dark themes. --bs-btn-focus-shadow-rgb is unset on all three.

Expected behavior

Each button shows a visible keyboard focus indicator, as the surrounding controls do.

Your environment

  • IDE: none, Quarto CLI at the command line
  • OS: macOS 26.5.2 (build 25F84)
  • Browser: Chromium 151.0.7922.34
  • Also present in Quarto 99.9.9, built from main at f9dd09cd2aee9f107a6c3f497e9210d55ceeebca

Quarto check output

Quarto 1.10.18
[✓] Checking environment information...
      Quarto cache location: /Users/charlottewickham/Library/Caches/quarto
[✓] Checking versions of quarto binary dependencies...
      Pandoc version 3.10.0: OK
      Dart Sass version 1.101.0: OK
      Deno version 2.7.14: OK
      Typst version 0.15.1: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
      Version: 1.10.18
      Path: /Applications/quarto/bin
[✓] Checking tools....................OK
      TinyTeX: v2026.04
      Chrome Headless Shell: 150.0.7871.115
      VeraPDF: 1.28.2
[✓] Checking LaTeX....................OK
      Using: TinyTex
      Path: /Users/charlottewickham/Library/TinyTeX/bin/universal-darwin
      Version: 2026
[✓] Checking Chrome Headless....................OK
      Using: Chrome Headless Shell installed by Quarto
      Path: /Users/charlottewickham/Library/Application Support/quarto/chrome-headless-shell/chrome-headless-shell-mac-arm64/chrome-headless-shell
      Version: 150.0.7871.115
[✓] Checking basic markdown render....OK
[✓] Checking R installation...........OK
      Version: 4.6.0
      Path: /Library/Frameworks/R.framework/Versions/4.6/Resources
      LibPaths:
        - /Users/charlottewickham/Library/R/arm64/4.6/library
        - /Library/Frameworks/R.framework/Versions/4.6/Resources/library
      knitr: 1.51
      rmarkdown: 2.31
[✓] Checking Knitr engine render......OK
[✓] Checking Python 3 installation....OK
      Version: 3.12.2
      Path: /Users/charlottewickham/.pyenv/versions/3.12.2/bin/python3
      Jupyter: 5.9.1
      Kernels: python3
[✓] Checking Jupyter engine render....OK
[✓] Checking Julia installation...

AI helped with this investigation. The work was grounded in a local clone of quarto-cli, as described in CONTRIBUTING.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilitybugSomething isn't workinghtmlIssues with HTML and related web technology (html/css/scss/js)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions