I have:
Bug description
With toc-location: left-body or toc-location: right-body, Quarto renders the
table of contents twice. The useDoubleToc branch clones the whole nav and
renames only the nav itself, from TOC to TOC-body:
|
if (useDoubleToc) { |
|
// Clone the TOC |
|
// Leave it where it is in the document, and just mutate it |
|
const clonedToc = toc.cloneNode(true) as Element; |
|
clonedToc.id = "TOC-body"; |
|
const tocActionsEl = clonedToc.querySelector(".toc-actions"); |
|
if (tocActionsEl) { |
|
tocActionsEl.remove(); |
|
} |
|
|
|
toc.parentElement?.insertBefore(clonedToc, toc); |
|
} |
Every other id inside the clone is copied as is. The page then holds two
elements for each id in the TOC: the h2#toc-title heading, and one
a#toc-<section> link per entry. The id attribute must be unique. From the
HTML Standard, The id attribute:
When specified on HTML elements, the id attribute value must be unique
amongst all the IDs in the element's tree and must contain at least one
character.
The duplicate ids are old. #14813 makes one of them load-bearing. That PR labels
the TOC landmark with aria-labelledby="toc-title":
|
<nav id="$idprefix$TOC" role="doc-toc"$if(toc-title)$ aria-labelledby="$idprefix$toc-title"$endif$> |
|
$if(toc-title)$ |
|
<h2 id="$idprefix$toc-title">$toc-title$</h2> |
|
$endif$ |
|
$table-of-contents$ |
|
</nav> |
Both nav elements now point at an id that resolves to one of two elements.
This is not a name bug, because both headings carry the same text. It does turn
an inert duplicate id into an ARIA reference. In axe-core 4.10.3, the version
Quarto vendors, duplicate-id-aria moves from inapplicable to needs review
(impact critical) on the same page.
This came out of review on #14813 (thanks @cderv). The duplicate id predates
that PR — measured below.
Steps to reproduce
---
title: "Left-body TOC"
format:
html:
toc: true
toc-location: left-body
toc-title: "On this page"
---
## First section
Some text.
## Second section
More text.
Then count the ids in the output:
quarto render index.qmd
grep -c 'id="toc-title"' index.html
Actual behavior
Two elements carry id="toc-title", and each TOC link id appears twice:
<nav id="TOC" role="doc-toc" aria-labelledby="toc-title" class="toc-active">
<h2 id="toc-title">On this page</h2>
<ul>
<li><a href="#first-section" id="toc-first-section" ...>First section</a></li>
...
<nav id="TOC-body" role="doc-toc" aria-labelledby="toc-title">
<h2 id="toc-title">On this page</h2>
<ul>
<li><a href="#first-section" id="toc-first-section">First section</a></li>
A DOM count of duplicated ids on that page gives
toc-title, toc-first-section, toc-second-section — one entry per TOC line,
plus the heading. The count grows with the size of the TOC.
axe-core 4.10.3 over the rendered page, in headless Chromium 151, Firefox 153
and WebKit 26.5:
| rendered with |
duplicate-id-aria |
landmark-unique |
current main |
inapplicable |
violation (moderate) |
| #14813 branch |
incomplete, critical |
violation (moderate) |
landmark-unique fires either way, so it is not a regression from #14813.
duplicate-id-aria is new, because toc-title only becomes an accessible
reference once aria-labelledby points at it. The rule reports as incomplete
rather than a violation because it carries reviewOnFail: true.
All three engines resolve the reference the same way, to the first element in
tree order — the heading inside nav#TOC. Both landmarks compute the same
accessible name, "On this page". Nothing user-facing breaks today. The only
id-based lookup in Quarto's own JS is
titleSelector: "#toc-title",
which reads document.body.querySelector("#toc-title") and so takes the first
match. Both headings hold identical text, so the sidebar toggle reads correctly
by accident.
Expected behavior
Each id appears once. The clone gets its own heading id, and its
aria-labelledby points at that id:
<nav id="TOC-body" role="doc-toc" aria-labelledby="toc-title-body">
<h2 id="toc-title-body">On this page</h2>
Both landmarks keep the accessible name "On this page". That shared name is
correct here, and it is what the ARIA Authoring Practices Guide asks for. From
Landmark Regions, Step 3: Label areas:
There is one rare circumstance where providing the same label to multiple
instances of a landmark can be beneficial: the content and purpose of each
instance is identical.
and, in the same document, on navigation landmarks:
If a navigation landmark has an identical set of links as another
navigation landmark on the page, use the same label for each navigation
landmark.
So the fix is the id, not the label. The two TOCs must keep the same name, which
means axe's landmark-unique result stays. That rule is best-practice, not
WCAG, and this is the case the APG carves out.
The duplicated a#toc-<section> link ids are the same fault from the same clone.
They are not ARIA references, and axe's duplicate-id rule is deprecated and
off by default, so they produce no finding. They are still invalid HTML. Worth
handling in the same place, or in a follow-up.
Your environment
- Positron 1.124.0
- macOS 26.5.2 (build 25F84)
Reproduced with released Quarto 1.10.18 and with a dev build of 1.11.
An AI assistant helped investigate, grounded in a local clone
(CONTRIBUTING.md, "Using AI tools to investigate").
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.5.2
Path: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources
LibPaths:
- /Users/charlottewickham/Library/R/arm64/4.5/library
- /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
knitr: 1.51
rmarkdown: 2.30
[✓] 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...
I have:
Bug description
With
toc-location: left-bodyortoc-location: right-body, Quarto renders thetable of contents twice. The
useDoubleTocbranch clones the wholenavandrenames only the
navitself, fromTOCtoTOC-body:quarto-cli/src/format/html/format-html-bootstrap.ts
Lines 352 to 363 in abc6a78
Every other id inside the clone is copied as is. The page then holds two
elements for each id in the TOC: the
h2#toc-titleheading, and onea#toc-<section>link per entry. Theidattribute must be unique. From theHTML Standard, The
idattribute:The duplicate ids are old. #14813 makes one of them load-bearing. That PR labels
the TOC landmark with
aria-labelledby="toc-title":quarto-cli/src/resources/formats/html/pandoc/toc.html
Lines 1 to 6 in 4715317
Both
navelements now point at an id that resolves to one of two elements.This is not a name bug, because both headings carry the same text. It does turn
an inert duplicate id into an ARIA reference. In axe-core 4.10.3, the version
Quarto vendors,
duplicate-id-ariamoves from inapplicable to needs review(impact
critical) on the same page.This came out of review on #14813 (thanks @cderv). The duplicate id predates
that PR — measured below.
Steps to reproduce
Then count the ids in the output:
quarto render index.qmd grep -c 'id="toc-title"' index.htmlActual behavior
Two elements carry
id="toc-title", and each TOC link id appears twice:A DOM count of duplicated ids on that page gives
toc-title,toc-first-section,toc-second-section— one entry per TOC line,plus the heading. The count grows with the size of the TOC.
axe-core 4.10.3 over the rendered page, in headless Chromium 151, Firefox 153
and WebKit 26.5:
duplicate-id-arialandmark-uniquemainlandmark-uniquefires either way, so it is not a regression from #14813.duplicate-id-ariais new, becausetoc-titleonly becomes an accessiblereference once
aria-labelledbypoints at it. The rule reports as incompleterather than a violation because it carries
reviewOnFail: true.All three engines resolve the reference the same way, to the first element in
tree order — the heading inside
nav#TOC. Both landmarks compute the sameaccessible name, "On this page". Nothing user-facing breaks today. The only
id-based lookup in Quarto's own JS is
titleSelector: "#toc-title",which reads
document.body.querySelector("#toc-title")and so takes the firstmatch. Both headings hold identical text, so the sidebar toggle reads correctly
by accident.
Expected behavior
Each id appears once. The clone gets its own heading id, and its
aria-labelledbypoints at that id:Both landmarks keep the accessible name "On this page". That shared name is
correct here, and it is what the ARIA Authoring Practices Guide asks for. From
Landmark Regions, Step 3: Label areas:
and, in the same document, on navigation landmarks:
So the fix is the id, not the label. The two TOCs must keep the same name, which
means axe's
landmark-uniqueresult stays. That rule isbest-practice, notWCAG, and this is the case the APG carves out.
The duplicated
a#toc-<section>link ids are the same fault from the same clone.They are not ARIA references, and axe's
duplicate-idrule is deprecated andoff by default, so they produce no finding. They are still invalid HTML. Worth
handling in the same place, or in a follow-up.
Your environment
Reproduced with released Quarto 1.10.18 and with a dev build of 1.11.
An AI assistant helped investigate, grounded in a local clone
(CONTRIBUTING.md, "Using AI tools to investigate").
Quarto check output