quarto-dev / quarto-dev/quarto-cli
Dashboards produce no heading elements
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 6k
- Forks
- 458
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 41
Description
Authors want, and readers need, real headings
Navigating by headings is the most commonly reported way screen reader users find information on a page (WebAIM screen reader surveys). A rendered format: dashboard document contains zero <h1>–<h6> elements: the dashboard title renders as div.navbar-title, page titles as tab links, card titles as div.card-title, and markdown headings are consumed by layout. For a screen reader user, a dashboard that looks richly structured is a single undifferentiated block.
Authors are trying to do the right thing and being silently undone:
- Markdown headings at every level (2–6) in flow content are treated as layout boundaries and their text is discarded from the output. In the canonical
## Row/### Chart Onepattern, "Chart One" appears nowhere in the rendered HTML. - Markdown headings inside a
::: {.card}div survive, but are converted to<div class="h4">etc. — styled like headings, invisible to heading navigation. - Card titles (
title=/#| title:) visually function as headings but are divs, which is a WCAG 2.2 SC 1.3.1 Info and Relationships failure: structure conveyed visually is not programmatically determinable.
The current workaround
Raw HTML heading tags inside cards pass through untouched:
---
title: "Website Analytics"
format: dashboard
---
# Traffic
## Row
::: {.card title="About this report"}
<h2>How we measure traffic</h2>
This report summarizes visits recorded by our analytics platform.
<h3>Data sources</h3>
Counts combine server logs and client-side events.
:::
::: {.card title="Visits by day"}
Weekday visits are roughly double weekend visits.
:::
# Search
This renders with exactly two heading elements: the raw <h2> and <h3>.
Problems with the workaround
- The outline starts at h2 with no h1 anywhere, and there is nothing to anchor levels to — the author picks numbers blind. Automated checkers flag this (axe
page-has-heading-one,heading-order). - The outline contradicts the visual hierarchy. The card title "About this report" sits visually above "How we measure traffic" but is a div, so heading navigation lands on the h2 with no context. Page names ("Traffic", "Search") are absent entirely.
- No style normalization. Markdown headings in cards are deliberately renormalized to card-appropriate sizes (shallowest →
.h4); raw tags bypass this, so a raw<h2>renders at full document scale inside a small card.
Proposed behavior
One principle: text already visible to sighted users becomes a real heading automatically; invisible text joins the outline only by explicit opt-in. A hypothetical dashboard:
---
title: "Website Analytics"
format: dashboard
---
# Traffic
## Row
Value boxes here — purely visual arrangement, no heading wanted.
## Where visitors come from {.sr-heading}
::: {.card title="Visits by day"}
Weekday visits are roughly double weekend visits.
### Data sources
Counts combine server logs and client-side events.
:::
# Search
Heading tree while the "Traffic" page is active:
h1 Traffic ← page title, visually hidden
│ (## Row: no attribute → not in the outline)
└── h2 Where visitors come from ← opted-in row, visually hidden
└── h3 Visits by day ← card title, visible, real element
└── h4 Data sources ← card-body heading, renumbered from ###
The "Search" page's h1 exists in the DOM but its tab panel is display: none (Bootstrap .tab-content > .tab-pane { display: none }), so exactly one h1 is exposed to assistive technology at a time.
Design details
- Page title → visually hidden
h1inside each page's tab panel (Bootstrap's.visually-hiddenis already bundled; zero visual change). A pageless dashboard is a single page whose title is the dashboard title, so single- and multi-page follow one rule. The navbar title stays a non-heading in thebannerlandmark — exactly how Quarto websites already treat the site title vs. the pageh1. - Card titles → real heading elements, one level below their nearest in-outline ancestor (h2 in the common case). Keep the existing classes (
<h2 class="card-title">): Bootstrap declares heading rules as element/class pairs (h4, .h4 { … }), so framework styling and class-based user CSS are unchanged. - Layout headings join the outline only with an explicit attribute (e.g.
{.sr-heading}), rendered as a visually hidden heading one level below the nearest in-outline ancestor. Containers without it — implicit, or namedRow/left/anything — stay outline-transparent, like layout divs in plain HTML. This is backwards compatible by construction (no existing dashboard changes visually or aurally), avoids guessing intent from throwaway names, gives purely-visual rows (value-box strips) no heading, and handles arbitrarily deep row/column nesting without exhausting h1–h6. - Card-body markdown headings → real
<h*>elements. The per-card renumbering machinery already normalizes the shallowest heading to level 4; keep it, but make the base dynamic (one below the card title's level) instead of the hardcoded4, emit real tags with the.hNclasses, and address the cap-at-6 collision (currently###and####in a card starting at#both flatten to.h6). - Warn when layout-heading text is discarded. Today it vanishes silently. A warning ("layout heading text is discarded; add
{.sr-heading}to expose it to assistive technology") makes the attribute discoverable; the documentedRow/Columnplaceholders silence it.
If compatibility is a concern despite the above, the whole behavior could ship behind a format option before becoming the default.
Where this happens in the source
- Flow headings consumed as layout (any level, text discarded):
quarto-post/dashboard.lua#L407-L414 - In-card headings converted to divs:
modules/dashboard/card.lua#L48-L66, with the level-4 normalization atcard.lua#L228-L236(introduced in ff83f7351 for visual consistency; the loss of semantics looks like a side effect).
Behavior above observed with Quarto 1.10.12. AI assistance was used to investigate, grounded in a local clone of this repository (per CONTRIBUTING.md).
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 quarto-post/dashboard.lua#L407-L414 and modules/dashboard/card.lua#L48-L66, then inspect the level-4 normalization at card.lua#L228-L236. Compare rendered dashboard HTML with the proposed heading tree, including page titles, card titles, opted-in layout headings, and nested card headings. Done means visible structure is represented by real headings, discarded layout text warns, and purely visual layout remains outside the outline.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- html, lua
- Domain
- accessibility
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100