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

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions