Skip to content

Give every website and book nav landmark a distinct, localizable label - #14813

Open
cwickham wants to merge 2 commits into
mainfrom
feature/nav-landmark-labels
Open

Give every website and book nav landmark a distinct, localizable label#14813
cwickham wants to merge 2 commits into
mainfrom
feature/nav-landmark-labels

Conversation

@cwickham

Copy link
Copy Markdown
Member

Description

Closes #14376.

Website and book pages emit up to six <nav> landmarks. Before this PR, only the breadcrumbs had a label, and it was hardcoded English (aria-label="breadcrumb"). Assistive technology listed the rest as identical "navigation" entries, and axe flagged the pages under landmark-unique.

This PR gives each landmark a distinct label from a new set of language keys:

Landmark Label
Navbar navigation-main-label ("Main")
Sidebar Adaptive: "Main" when the site has no navbar, "Section" when it has one
Secondary/mobile nav strip navigation-secondary-label ("Secondary")
Previous/next page nav navigation-page-label ("Page")
Breadcrumbs (both copies) navigation-breadcrumbs-label ("Breadcrumbs")
Table of contents aria-labelledby on its existing localized heading ("On this page") — no new key

Authors can override any key with language: metadata, per document or site-wide.

Label wording

The default labels do not contain the word "navigation". Screen readers announce the landmark role after the label, so a label like "Main navigation" reads as "Main navigation, navigation". The ARIA Authoring Practices Guide says not to use the role as part of the label, and the WAI page-structure tutorial uses the bare label "Main" for a nav region. VoiceOver testing confirmed the double announcement with the longer wording.

Design notes

  • The two breadcrumb copies share one label. They hold identical links, and only one is in the accessibility tree at a time (one shows at lg and up, the other below). The APG also asks identical navigation sets to share a label.
  • The secondary nav keeps a label distinct from the sidebar. Both are visible together when the mobile sidebar is open.
  • The TOC label reuses the visible heading via aria-labelledby, which is the pattern the WAI tutorial recommends. The TOC nav can also hold the Other Formats / code-links / other-links sections. The label stays acceptable: everything in that nav is page-scoped.
  • The TOC change also applies to revealjs (same partial pattern, toc-slide.html).

Out of scope

Documentation

No documentation PR: the only user-facing change is the new language keys, and the docs already refer users to _language.yml itself as the reference for available keys (Document Language).

Testing

  • New smoke-all tests assert every label, the adaptive sidebar branch (with and without navbar), a language: override, and a custom toc-title in both html and revealjs.
  • Verified axe landmark-unique passes on navbar-only, sidebar-only, navbar+sidebar, and book sites, at desktop and narrow viewports, including with the mobile sidebar open. Also reviewed the in-page report from axe: output: document on the rendered site. These browser checks were manual; this PR does not add a Playwright case for them.
  • Verified the VoiceOver landmark rotor lists each landmark with a distinct name.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

#14376)

New language keys navigation-{main,section,secondary,page,breadcrumbs}-label
label the navbar, sidebar (adaptive), secondary nav, prev/next page nav, and
breadcrumbs; the TOC nav is labelled by its localized heading via
aria-labelledby (html + revealjs). Defaults omit the word "navigation" per the
APG landmark-regions practice, since screen readers announce the role after
the label.

Note: all-schema-definitions.json also picks up the lagged cache-globals
schema from #14735 (regenerated artifacts trail schema changes by one build).
@posit-snyk-bot

posit-snyk-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cwickham
cwickham marked this pull request as ready for review August 26, 2026 18:06
@cwickham
cwickham requested a review from cderv August 26, 2026 18:06

@cderv cderv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I checked out the branch and ran both new smoke-all suites, they pass here on Windows. I also ran axe-core against the rendered fixture site in Chrome, landmark-unique, landmark-one-main, region and duplicate-id-aria are all clean at 1400x900 and at 500x900, including with the mobile sidebar expanded. The shared breadcrumbs label holds up, .quarto-secondary-nav is display: none from lg up and .quarto-title-breadcrumbs carries d-none d-lg-block, so only one of the two is ever in the accessibility tree.

One thing I noticed while looking at the TOC change. With toc-location: left-body or right-body we clone the whole TOC nav into the body and only rename the nav id:

// 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);

The <h2 id="toc-title"> inside is cloned as is, so the page ends up with two elements sharing that id, and now both navs point at it with aria-labelledby. The name resolves to the same title for both either way, and I measured landmark-unique already failing on that page before your change, so it is not a regression from this PR. Still, the ARIA reference now depends on a duplicated id. Would you rather fold a fix in here, or should I file it separately ?

Nit, no need to act on it: language[kNavigationBreadcrumbsLabel] || "Breadcrumbs" is the only language lookup in website-navigation.ts with a hardcoded fallback, the others rely on the resolved defaults.

The new keys and the translation files

This part is not really about your change. The five new keys land in _language.yml only, so every translated site gets English landmark names sitting next to otherwise translated chrome. I looked at the wider picture and 25 of the 30 base language files are already behind _language.yml, so I filed #14822 for that separately.

For these five keys I produced a candidate set for all 30 base language files. To be explicit about what that is: it is LLM generated. I do not speak most of these languages and no native speaker has looked at any of it. What I verified is only the mechanics, lang: fr renders aria-label="Principale" and lang: zh renders aria-label="主要", so the keys resolve and the encoding is fine. That says nothing about whether the wording is right.

Every row wants a review before it ships. The de, is, tr, ko and eu ones are the ones I trust least, Main and Secondary are bare adjectives and those languages want a head noun after them. The breadcrumbs column is the least settled everywhere, there is no single agreed term.

So it is your call. We could take this as a starting point here and be clear in the commit that the strings are machine generated and unreviewed, or leave the five keys English in this PR and let #14822 collect translations from people who actually speak the languages. Which do you prefer ?

Candidate translations, LLM generated and unreviewed (30 base languages)

These are LLM generated and have not been checked by a native speaker. The warning sign marks the rows I trust least, where "main" and "secondary" do not stand alone as adjectives.

main section secondary page breadcrumbs
bg Основна Раздел Второстепенна Страница Път
ca Principal Secció Secundària Pàgina Ruta
cs Hlavní Sekce Sekundární Stránka Cesta
da Primær Sektion Sekundær Side Brødkrummesti
de ⚠️ Primär Abschnitt Sekundär Seite Brotkrümelpfad
el Κύρια Ενότητα Δευτερεύουσα Σελίδα Διαδρομή
es Principal Sección Secundaria Página Ruta
eu ⚠️ Nagusia Atala Bigarren mailakoa Orria Ibilbidea
fi Ensisijainen Osio Toissijainen Sivu Polku
fr Principale Section Secondaire Page Fil d'Ariane
he ראשי מקטע משני עמוד נתיב
id Utama Bagian Sekunder Halaman Jejak
is ⚠️ Aðal Undirkafli Auka Síða Slóð
it Principale Sezione Secondaria Pagina Percorso
ja メイン セクション サブ ページ パンくずリスト
ko ⚠️ 주요 섹션 보조 페이지 이동 경로
lt Pagrindinė Skiltis Antrinė Puslapis Kelias
nb Primær Seksjon Sekundær Side Brødsmulesti
nl Primair Sectie Secundair Pagina Kruimelpad
nn Primær Bolk Sekundær Side Brødsmulesti
pl Główna Sekcja Dodatkowa Strona Ścieżka
pt Principal Seção Secundária Página Trilha
ru Основная Раздел Дополнительная Страница Путь
sk Hlavná Sekcia Sekundárna Stránka Cesta
sl Glavna Razdelek Drugotna Stran Pot
sr-Latn Glavna Odeljak Sekundarna Stranica Putanja
sv Primär Sektion Sekundär Sida Brödsmulor
tr ⚠️ Ana Bölüm İkincil Sayfa Yol
ua Основна Розділ Додаткова Сторінка Шлях
zh 主要 章节 次要 页面 面包屑

All five avoid that language's own word for "navigation", following the rule you documented in _language.yml. That comment only lives in _language.yml though, so a translator editing _language-de.yml later will not see it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a11y: navbar and sidebar nav landmarks have no aria-label (landmark-unique)

3 participants