Skip to content

a11y: secondary text (blockquotes, captions, asides) fails WCAG AA color contrast in cosmo  #14678

Description

@cwickham

I have:

Bug description

The body-secondary mixin, which colors blockquotes, figure/table captions, header section numbers, and margin/aside text, derives its color as theme-dim($body-color, 25%) with no contrast check:

@mixin body-secondary {
@if variable-exists(body-secondary) {
color: $body-secondary;
} @else {
color: theme-dim($body-color, 25%);
}
}

For theme: cosmo this yields #747a7f on #ffffff = 4.34:1, below the WCAG 2.0 AA (1.4.3) minimum of 4.5:1 — axe-core flags it as color-contrast, impact serious. On quarto.org (a cosmo-derived theme with $body-color: #343a40) the same derivation yields #6d7a86 = 4.39:1, which is how this surfaced in an axe audit of the site.

All five @include body-secondary sites are affected (_bootstrap-rules.scss lines 663, 695, 702, 710, 913), e.g. the blockquote rule:

.blockquote {
font-size: inherit;
padding-left: 1rem;
padding-right: 1.5rem;
@include body-secondary;
}

Quarto already has contrast-guarding machinery — theme-contrast() / accessibleContrast() with an explicit "AA" (4.5:1) level — used for navbar, footer, sidebar, and button foregrounds:

@function theme-contrast($color, $bgColor, $level: "AAA") {
// These will be defined in bootstrap, but creating values here
// That will make this function accessible to callers prior to bootstrap variables
// being set
$black: rgb(0, 0, 0) !default;
$white: rgb(255, 255, 255) !default;
@if tone($bgColor) == "light" {
@return accessibleContrast($color, $black, $bgColor, $level);
} @else {
@return accessibleContrast($color, $white, $bgColor, $level);
}
}
@function accessibleContrast($startColor, $mixColor, $bgColor, $level: "AAA") {
// A: 3:1
// AA: 4.5:1
// AAA: 7:1
$goalContrastRatio: 3;
@if $level == "AA" {
$goalContrastRatio: 4.5;
} @else {
$goalContrastRatio: 7;
}
$percentMix: 100;
$contrastRatio: 0;
$contrastColor: null;
@while ($contrastRatio < $goalContrastRatio and $percentMix > 0) {
$contrastColor: mix(
$startColor,
$mixColor,
percentage(quarto-math.div($percentMix, 100))
);
$contrastRatio: quarto-contrast($contrastColor, $bgColor);
$percentMix: $percentMix - 1;
}
@return $contrastColor;
}

Routing the mixin's fallback through it, e.g. theme-contrast(theme-dim($body-color, 25%), $body-bg, "AA"), would keep the dimmed look while guaranteeing conformance for every theme.

Two notes:

  • The mixin already honors a $body-secondary SCSS variable when defined — quarto.org uses that as a workaround (Fix WCAG AA color contrast: muted text and appendix links (light theme) quarto-web#2102). It appears to be undocumented; worth documenting regardless of the fix here.
  • Not every built-in theme fails: the default theme's body color dims to #5a6570 = 5.95:1, which passes. The failure depends on where $body-color lands, which is exactly why a contrast guard rather than a per-theme tweak seems right.

Related: the axe-finding family in #14373 and the accessibility epic #8706.

Investigated with AI assistance grounded in a local clone of quarto-cli (per CONTRIBUTING.md, "Using AI tools to investigate").

Steps to reproduce

---
title: repro
format:
  html:
    theme: cosmo
    axe:
      output: document
---

> A blockquote.

Render, open in a browser.

Actual behavior

The blockquote text renders at #747a7f on #ffffff = 4.34:1. axe-core reports:

Serious · WCAG 2.0 AA (1.4.3): Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
Elements must meet minimum color contrast ratio thresholds
p

The same color applies to figcaption/caption/.table-caption, .header-section-number, and margin/aside text.

Expected behavior

Text colors derived by Quarto for built-in themes meet the WCAG AA 4.5:1 minimum, e.g. by passing the body-secondary fallback through the existing theme-contrast(..., "AA") machinery.

Your environment

  • IDE: Positron 2026.07.0 (build 352)
  • OS: macOS 26.5.1 (build 25F80)

Quarto check output

Quarto 1.10.12
[✓] Checking environment information...
      Quarto cache location: /Users/charlottewickham/Library/Caches/quarto
[✓] Checking versions of quarto binary dependencies...
      Pandoc version 3.8.3: OK
      Dart Sass version 1.87.0: OK
      Deno version 2.7.14: OK
      Typst version 0.14.2: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
      Version: 1.10.12
      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...

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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