Repository navigation
Conversation
…HTML Replace the four focus indicators (browser ring, Bootstrap's translucent box-shadow halo, the sidebar toggle outline, and the search box's 1px shadow) with one ring after W3C technique C40: a 2px outline in the outer color, with the inner color as a box-shadow in the 2px offset. - New Sass variables $focus-ring-outer-color and $focus-ring-inner-color. When the outer color is derived from $primary-text-emphasis, it is darkened to 9:1 against the inner color. An explicit color is used as is. - accessibleContrast() accepts a numeric ratio, and "A" now gives 3:1. - Bootstrap's focus halos are turned off at the source. - The search box draws the ring on its form; pagination gets a (0,4,0) rule because vapor and zephyr replace its box-shadow. - The TOC sidebars get padding so they no longer clip the ring, and the secondary nav paints above the navbar so the ring's top edge shows.
Rename html-focus-indicator-bare-btn.spec.ts to html-focus-indicator.spec.ts and the bare-btn-focus fixture to focus-indicator. The fixture sets a distinctive outer color, so each test also catches a theme or component rule that wins the cascade. Adds the navbar, TOC, tabset, sidebar toggle, search, and listing sort/filter/pagination controls, and a forced-colors check.
Collaborator
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Lower the global :focus-visible rule to (0,1,0) so author rules such as a:focus-visible win, and re-include the ring at (0,2,0) only on the Bootstrap classes that remove their outline. Put the inner tone in --quarto-focus-ring-shadow so authors can remove it with one line. Document the ignored Bootstrap focus variables in the changelog, and test an author override.
MathJax injects mjx-speech:focus { outline: none } at runtime, after the
Quarto stylesheet. It beat the (0,1,0) global rule and left only the
white inner tone. A (0,2,1) rule puts the ring back.
Move $focus-ring-outline-width and $focus-ring-outline-offset to the defaults layer with !default, so authors can change the size of the ring. Test a theme that sets both.
The drop shadow needed its own focus rule to keep the ring's inner tone. Without it, the skip link gets the standard ring, and author overrides of the ring apply to it too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Closes #15139. Supersedes #14867.
Quarto's HTML output uses four different keyboard focus indicators. Two of them fail the 3:1 contrast minimum of WCAG 1.4.11. In forced-colors mode, some controls have no focus indicator at all. The issue gives the details and measurements.
This PR replaces the four indicators with one focus ring, defined in one place.
The ring
One rule,
:focus-visiblein_bootstrap-rules.scss, draws the ring for every focusable element. The ring follows W3C technique C40: two tones with at least 9:1 contrast between them. As a result, one of the two tones has at least 3:1 contrast against any solid background.outline, withoutline-offset: 2px.box-shadowof 2px that fills the offset.The outer tone is on the
outlinefor two reasons. Bootswatch themes often setbox-shadowon controls, but they rarely setoutline. Forced-colors mode also keeps theoutlineand removes thebox-shadow.default:cosmo(e.g. used on quarto.org):Tone order follows the page
The two tones are white and a dark tone, derived from
$primary-text-emphasis. On a light page, the dark tone is the outer ring. On a dark page, the tones change places, so the white tone is the outer ring. Thus the outer ring always contrasts with the page.A page is dark if
$body-bghas more contrast with white than with black. In the built-in themes, seven themes are dark: cyborg, darkly, quartz, slate, solar, superhero, and vapor.cyborg:darkly:Contrast between the tones
The derived pair always has at least 9:1 contrast, in all themes:
$primary-text-emphasisalready has at least 9:1 contrast against white. The lowest is minty, at 9.13:1. The highest is lux, at 19.80:1.$primary, which gives less than 9:1. In this case, Quarto darkens the derived tone only as much as necessary to get 9:1. For example,$primary: #ffd54fgoes from 7.26:1 to 9.16:1.How authors change the ring
Users can set the two tones with
$focus-ring-outer-colorand$focus-ring-inner-color. If a user sets one or both variables, Quarto uses the colors as given:If a user sets only the inner color, Quarto still derives the outer color and adjusts it to 9:1 against the inner color.
Users can set the size of the ring with
$focus-ring-outline-widthand$focus-ring-outline-offset(both 2px by default). The inner tone always fills the offset. Quarto adds padding to scroll containers from these two values, so a larger ring is not clipped. A width of less than 2px does not meet WCAG 2.4.13.Author focus rules still win over the ring:
a:focus-visible(0,1,1) win over it. Many sites have rules like this one.box-shadow: var(--quarto-focus-ring-shadow). Most author rules set onlyoutline, so the inner tone stays inside the author ring. To remove it, add--quarto-focus-ring-shadow: noneto the author rule or to:root.Quarto no longer uses Bootstrap's
$focus-ring-color,$focus-ring-box-shadow,$btn-focus-width, and$btn-focus-box-shadow. The changelog tells authors to use the new variables and the custom property instead.Other changes
$focus-ring-box-shadow,$form-select-focus-box-shadow, and$form-range-thumb-focus-box-shadowarenone, and$btn-focus-widthis0.outline: revertfrom Restore keyboard focus indicator on bare .btn buttons #14785, and theoutline: nonerules inquarto-search.scss.accessibleContrast()accepts a number. Also,"A"now gives 3:1. Before, it gave 7:1. No caller uses"A".padding-right: $focus-ring-extent, so that they do not clip the ring..navbar-nav-scroll) getspadding: $focus-ring-extentand an equal negative margin. Before, it clipped the sides of the ring.position: relative, so that the top edge of the ring shows above the navbar.Exceptions to "one rule"
.btn:focus-visible { outline: 0 }and.form-control:focus. These rules win over the (0,1,0) global rule. A second rule includes the ring again on these classes:.btn,.btn-close,.nav-link,.navbar-toggler,.accordion-button,.carousel-control-prev,.carousel-control-next,.form-control,.form-select,.form-check-input, and.form-range. It ties with Bootstrap and comes later. Author rules on these classes come later again, so they still win.mjx-speech:focus { outline: none }at runtime, after the Quarto stylesheet. A(0,2,1)rule,:root mjx-speech:focus-visible, puts the ring back on each equation..aa-Form, which holds a borderless.aa-Input. The ring goes on the form when the input has keyboard focus..page-linkabox-shadowwith a higher specificity. A(0,4,0)rule puts the ring back.Known limits
.btn:focus { outline: none }. At 800px, the sidebar toggle and the search button have no ring, before and after this PR. I will open a separate issue.box-shadowoverrides. Five themes replace the inner tone on some controls. The outer tone stays on all of them:Tests and measurements" with this section
Tests and measurements
All measurements come from renders and computed styles, in Chromium and WebKit.
html-focus-indicator.spec.ts, with the fixturestests/docs/playwright/website/focus-indicator/,tests/docs/playwright/html/focus-ring-author-override.qmd, andtests/docs/playwright/html/focus-ring-size.qmd. In Chromium and WebKit, 29 tests pass and 1 test skips (forced colors is Chromium-only). On release 1.11.6, the 13 tests other than the author-override and size tests all fail.a:focus-visibleoutline and--quarto-focus-ring-shadow: none. The test makes sure that the link shows the author outline and no inner tone.<input>. This is by design, because the ring is on its form.box-shadowoverrides above.toc-leftat 1400px and 800px, and the collapsed navbar menu at 700px.main. With this PR, they have one.quarto call axeshows no new or changed findings, compared withmain, on default, cosmo, darkly, and quartz.Checklist
I have (if applicable):
AI-assisted PR