quarto-dev / quarto-dev/quarto-cli

Dashboards produce no heading elements

Open
#14,698 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

accessibility dashboards enhancement
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 One pattern, "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

  1. Page title → visually hidden h1 inside each page's tab panel (Bootstrap's .visually-hidden is 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 the banner landmark — exactly how Quarto websites already treat the site title vs. the page h1.
  2. 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.
  3. 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 named Row/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.
  4. 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 hardcoded 4, emit real tags with the .hN classes, and address the cap-at-6 collision (currently ### and #### in a card starting at # both flatten to .h6).
  5. 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 documented Row/Column placeholders 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

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.