quarto-dev / quarto-dev/quarto-cli
a11y: `page-layout: custom` renders the title block header outside `<main>`
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 6k
- Forks
- 458
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 41
Description
I have:
- searched the issue tracker for similar issues
- installed the latest version of Quarto CLI
- formatted my issue following the Bug Reports guide
Bug description
On a page that uses page-layout: custom, Quarto's own title block (#title-block-header) is rendered outside the page's <main> landmark, so it implicitly becomes a second banner landmark alongside the navbar's #quarto-header.
#title-block-header is a <header>, and a <header> computes as banner unless it is inside <main> or sectioning content (HTML-AAM §3.5.49). With the default layout the title block renders inside main#quarto-document-content, so it is not a banner and the page has one. With page-layout: custom there is no <main> at all, so the title block is never nested.
This is the same problem as #14375, #14377 and #14605 — #title-block-header ending up outside <main> — but a different code path. In #14375 the title-block post-processor moves the header out of #quarto-content when title-block-banner: true or title-block-style: manuscript is set. In #14605 the about post-processor re-emits it as a sibling of <main>. Here nothing moves it: the custom layout never opens a <main> for it to sit in.
before-body-article.ejs opens <main class="content">, while before-body-custom.ejs opens only a div, and formatHasArticleLayout selects between them:
- https://github.com/quarto-dev/quarto-cli/blob/7eebb00b94674e7f7c6b23c2e994b352b912e7c5/src/resources/formats/html/templates/before-body-article.ejs#L20
- https://github.com/quarto-dev/quarto-cli/blob/7eebb00b94674e7f7c6b23c2e994b352b912e7c5/src/resources/formats/html/templates/before-body-custom.ejs#L1
- https://github.com/quarto-dev/quarto-cli/blob/7eebb00b94674e7f7c6b23c2e994b352b912e7c5/src/format/html/format-html-bootstrap.ts#L139-L161
Authors cannot work around this. page-layout: custom hands the body container to the author, so an author can add their own <main> and does get a main landmark. It does not help: Quarto emits #title-block-header before the author's content, so the title block stays outside whatever the author wraps, and the page still has two banners.
Steps to reproduce
A website with page-layout: custom.
_quarto.yml:
project:
type: website
website:
title: "Custom layout site"
navbar:
left:
- href: index.qmd
text: Home
format:
html:
page-layout: custom
index.qmd:
---
title: Custom layout in a website
---
Body text.
Render the site, open _site/index.html, and count the landmarks in the browser console:
document.querySelectorAll('header').length // 2, both computing as banner
document.querySelectorAll('main').length // 0
Removing page-layout: custom from _quarto.yml gives one banner and one main.
Actual behavior
Two banner landmarks and no main. Measured in Chromium with the same site rendered both ways:
default layout banners: 1 mains: 1
header#quarto-header parent=body insideMain=false
header#title-block-header parent=main#quarto-document-content insideMain=true
page-layout: custom banners: 2 mains: 0
header#quarto-header parent=body insideMain=false
header#title-block-header parent=div#quarto-content insideMain=false
axe reports two best-practice violations, both moderate:
landmark-no-duplicate-banner— "Document should not have more than one banner landmark"landmark-one-main— "Document should have one main landmark"
Adding an author-supplied <main> around the body content clears landmark-one-main (mains: 1) but not the duplicate banner (banners: 2).
A standalone document with page-layout: custom has one banner rather than two, since there is no navbar, but the body content still sits outside every landmark (region).
Expected behavior
The title block that Quarto emits does not become a second banner landmark.
Your environment
- IDE: Positron 2026.08.0 (build 272)
- OS: macOS 26.5.2 (build 25F84)
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...
Found while reviewing #14739. Part of #8706. AI-assisted investigation, grounded in a local clone of quarto-cli.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with before-body-article.ejs, before-body-custom.ejs, and formatHasArticleLayout in format-html-bootstrap.ts, then render the provided page-layout: custom reproduction. Inspect _site/index.html and compare its header and main landmarks with the default layout. Done means the custom layout has the expected main landmark and does not make #title-block-header a duplicate banner.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- accessibility, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100