Skip to content

No skip-to-content link: keyboard users must tab through all navigation on every page (WCAG 2.4.1, Level A) #14684

Description

@cwickham

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

Quarto's HTML outputs render navigation — navbar, search, sidebar, page tabs, TOC — before the main content, with no "skip to main content" link. Keyboard and switch users must therefore tab through every navigation stop on every page before reaching the content, which fails WCAG 2.2 SC 2.4.1 "Bypass Blocks" (Level A) — a criterion Section 508, EN 301 549, and the US ADA Title II rule all incorporate.

Authors have no remedy short of hand-injecting HTML (the announcement workaround from #11602 consumes that feature and leaves a visible bar), so I'm filing this as a bug rather than a feature request; the standing request is discussion #10096.

Most of the machinery already exists:

  • The target has shipped since 2022: 59616f22e added <main id="quarto-document-content"> explicitly "for things like skip links" (before-body-article.ejs#L20, nav-before-body.ejs#L128-L130); the link never followed.
  • Bootstrap's .visually-hidden-focusable helper (what Bootstrap's own docs use for their skip link) is already compiled into every Quarto theme bundle.
  • Exception: format: dashboard output has no <main> and no id on its content container, so there is currently nothing to target — itself a missing-landmark issue.

Related:

  • Discussion #10096 (Jun 2024): standing feature request; comments cite MkDocs Material and pkgdown as reference implementations.
  • Issue #11602 (Dec 2024): same report plus the workaround; converted to discussion #11603, duplicate of Skip to Main Content button #10096.

Steps to reproduce

Render each of the three documents below, open the output in a browser, click in the address bar, then press Tab repeatedly and observe which element receives focus at each step.

1. Document with a TOC (format: html):

---
title: "Reproducible Quarto Document"
format: html
toc: true
---

## Section one

Some text with a [link](https://quarto.org).

{{< lipsum 1 >}}

## Section two

{{< lipsum 1 >}}

## Section three

{{< lipsum 1 >}}

2. Multi-page dashboard (format: dashboard):

---
title: "Reproducible Quarto Dashboard"
format: dashboard
---

# Heat

## Row

Content about heat-related illness, with a [link](https://quarto.org).

# Air quality

## Row

Content about air quality.

# Water

## Row

Content about water quality.

3. Website with navbar and sidebar (quarto render on this project):

_quarto.yml:

project:
  type: website
website:
  title: "Reproducible Website"
  navbar:
    left:
      - href: index.qmd
        text: Home
      - href: about.qmd
        text: About
      - href: data.qmd
        text: Data
  sidebar:
    contents:
      - index.qmd
      - about.qmd
      - data.qmd

index.qmd (plus trivial about.qmd, data.qmd):

---
title: "Home"
---

Some text with a [link](https://quarto.org).

Actual behavior

There is no skip link anywhere; the first tab stops are always navigation. Measured tab order (Quarto 1.10.12, Chrome 150; the numbers are Tab presses from page load):

Document with TOC — content reached at stop 4:

1  <a#toc-section-one>    "Section one"
2  <a#toc-section-two>    "Section two"
3  <a#toc-section-three>  "Section three"
4  first link inside the document content

Dashboard — content reached at stop 3:

1  <a> navbar brand "Reproducible Quarto Dashboard"
2  <a#tab-heat.nav-link.active> "Heat"   (inactive page tabs are tabindex="-1")
3  first link inside the dashboard content

Website with navbar + 3-item sidebar — content reached at stop 9:

1  <a.navbar-brand>          "Reproducible Website"
2  <button> search
3  <a.nav-link.active>       "Home"
4  <a.nav-link>              "About"
5  <a.nav-link>              "Data"
6  <a.sidebar-link.active>   "Home"
7  <a.sidebar-link>          "About"
8  <a.sidebar-link>          "Data"
9  first link inside the page content

These are minimal examples; the count grows linearly with navigation size. A documentation site with a 40-item sidebar imposes ~45 tab stops per page, on every page, and the TOC case grows with document length.

Expected behavior

The standard skip-link pattern (as shipped by pkgdown, MkDocs Material, and Bootstrap's own documentation):

  1. Pressing Tab once on a freshly loaded page reveals a visible "Skip to main content" link as the first tab stop, before the navbar brand, search, TOC, or page tabs. This is the position WCAG's sufficient technique G1 requires — its test procedure is "check that a link is the first focusable control on the Web page" — and G1 explicitly permits the link to be visually hidden until focused, so nothing changes for mouse users.
  2. Activating it with Enter scrolls to the main content and moves the sequential focus point there, so the next Tab lands on the first interactive element inside the content (the body link in the repros above) rather than back in the navigation.
  3. Tabbing past the link without activating it hides it again.
  4. Screen readers announce it as the first link on the page.
  5. The link appears on every page of a website. For dashboards, one link per document suffices (multi-page dashboards are Bootstrap tab panes within a single document), but the content container needs a <main> element or at least an id to serve as the target — dashboard output currently has neither.

Expected tab order for the website repro would then be: stop 1 = skip link → Enter → next Tab = first link in content. Two keystrokes instead of nine — on a real docs site, two instead of dozens.

No JavaScript should be needed: following a same-page fragment link moves the sequential focus navigation starting point in all modern browsers; tabindex="-1" on the target covers older browser/AT combinations.

Your environment

  • IDE: Positron 2026.07.0 build 352
  • OS: macOS 26.5.1 (build 25F80)
  • Browser (keyboard testing): Google Chrome 150.0.7871.49

An AI assistant helped investigate this issue, grounded in a local clone of quarto-cli (per CONTRIBUTING.md, "Using AI tools to investigate"). Tab orders were verified with real keypresses in headless Chrome.

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.13.2
      Path: /Users/charlottewickham/.local/share/uv/python/cpython-3.13.2-macos-aarch64-none/bin/python
      Jupyter: (None)

      Jupyter is not available in this Python installation.
      Install with python3 -m pip install jupyter

[✓] Checking Julia installation...

Metadata

Metadata

Assignees

Labels

accessibilitybugSomething isn't workingdashboardshtmlIssues 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

Relationships

None yet

Development

No branches or pull requests

Issue actions