Skip to content

Add NavList.Heading slot for accessible section-nav headings#8031

Open
janmaarten-a11y wants to merge 4 commits into
mainfrom
janmaarten-a11y/navlist-heading-slot
Open

Add NavList.Heading slot for accessible section-nav headings#8031
janmaarten-a11y wants to merge 4 commits into
mainfrom
janmaarten-a11y/navlist-heading-slot

Conversation

@janmaarten-a11y

@janmaarten-a11y janmaarten-a11y commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Overview

Many GitHub pages built with NavList — user settings, repository settings, and similar — end up with a confusing or broken page heading structure. This PR adds an optional NavList.Heading slot that gives the whole navigation region a single heading, names the <nav> landmark, and automatically keeps group headings nested correctly beneath it.

Why this is needed

Today, NavList.Group headings default to h3 (and some are hand-tweaked to h2). On a typical settings page that means a subset of the left-hand section-navigation links is exposed at the same heading level as the main page-content sections. For someone navigating by heading with a screen reader, the navigation links and the page content look like siblings in the document outline, which misrepresents the actual hierarchy and makes the page slower to understand and move through.

What usually makes more sense is:

  • an h2 heading for the NavList as a whole, with
  • h3 subheadings for each NavList.Group.

That produces a correct h2 → h3 outline, lets screen reader users jump straight to the navigation region, and makes the visual/content hierarchy match the semantic one.

Right now teams have to wire all of this up by hand — add an accessible name to the <nav>, pick heading levels for each group, and remember to keep them consistent. It's easy to forget and easy to get wrong. This PR makes the correct structure the easy, built-in path.

How it works

  • NavList.Heading is a new slot. Add it as a child of NavList and it renders the heading for the region. It defaults to h2.
  • It labels the <nav> landmark automatically via aria-labelledby, so the navigation gets an accessible name without extra work. If you already pass your own aria-label/aria-labelledby, yours wins — we don't override it.
  • It supports a visuallyHidden variant for pages where the heading shouldn't be shown visually but should still exist for assistive tech and as the landmark's name.
  • Group headings auto-nest. When a NavList.Heading is present, NavList.Group (and NavList.GroupHeading) default to one level deeper than the heading — h3 under an h2, h4 under an h3. With no NavList.Heading, groups keep today's h3 default, so nothing changes for existing usage.
  • The heading level is intentionally constrained to h2 or h3 (no h1 — that's the page title — and group headings never go deeper than h4), keeping the outline shallow. If you need a specific level on a single group, pass NavList.GroupHeading as={...} as an explicit override.
<NavList>
  <NavList.Heading>Settings</NavList.Heading>          {/* h2, names the <nav> */}
  <NavList.Group title="Account">                      {/* auto h3 */}
    <NavList.Item href="#" aria-current="page">Profile</NavList.Item>
    <NavList.Item href="#">Appearance</NavList.Item>
  </NavList.Group>
  <NavList.Group title="Security">                     {/* auto h3 */}
    <NavList.Item href="#">Password and authentication</NavList.Item>
  </NavList.Group>
</NavList>

Implementation notes

  • NavList already wraps its children in ActionList, which has a heading slot. The new work is a NavList-level slot that (a) names the <nav> landmark and (b) coordinates heading levels with groups via a small internal context. It's mostly wiring rather than net-new machinery.
  • For the visuallyHidden variant, the visually-hidden styles are applied directly to the heading element (reusing the existing _VisuallyHidden CSS) rather than wrapping it in a <span> — a heading isn't valid phrasing content inside a span.
  • A follow-up is noted for ActionList.Heading, which currently has the same span-wrapping pattern; it's intentionally left out of this PR to keep the scope tight.

Changelog

New

  • NavList.Heading slot: names the navigation region, labels the <nav> landmark via aria-labelledby, supports a visuallyHidden variant, and defaults to h2 (configurable to h3).
  • NavList.Group / NavList.GroupHeading now default their heading level to one below a NavList.Heading (e.g. h3 under an h2).
  • Storybook stories: "With Heading" and "With Heading (hidden)".

Changed

  • When a NavList.Heading is present, NavList.Group title headings derive their level from it instead of always rendering h3. With no NavList.Heading, the h3 default is unchanged.

Removed

  • Nothing.

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Purely additive. Existing NavList usage that doesn't include a NavList.Heading renders exactly as before.

Testing & Reviewing

  • Unit tests cover: default h2 heading, as="h3" override, <nav> labelled by the heading, consumer aria-label not being overridden, group headings deriving h3/h4, the h3 fallback with no heading, NavList.GroupHeading as override, and the visually-hidden heading staying in the a11y tree without a wrapping span.
  • In Storybook see Components → NavList → Features → "With Heading" and "With Heading (hidden)". Inspect the accessibility tree to confirm the h2 → h3 outline and that the <nav> is named by the heading.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook)
  • Changes are SSR compatible
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge
  • (GitHub staff only) Integration tests pass at github/github-ui

@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9533be7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. If this doesn't work, you can also use the original workflow here. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

To publish a canary release for integration testing, apply the Canary Release label to this PR.

@primer

primer Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🤖 Lint issues have been automatically fixed and committed to this PR.

@github-actions github-actions Bot requested a deployment to storybook-preview-8031 June 23, 2026 06:46 Abandoned
@github-actions github-actions Bot temporarily deployed to storybook-preview-8031 June 23, 2026 06:56 Inactive
janmaarten-a11y and others added 2 commits June 23, 2026 00:00
Introduces an optional NavList.Heading that names the navigation region,
labels the <nav> landmark, and coordinates heading levels so group
headings nest correctly beneath it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot temporarily deployed to storybook-preview-8031 June 23, 2026 19:09 Inactive
@janmaarten-a11y janmaarten-a11y marked this pull request as ready for review June 23, 2026 19:12
@janmaarten-a11y janmaarten-a11y requested a review from a team as a code owner June 23, 2026 19:12
@janmaarten-a11y janmaarten-a11y added the Canary Release Apply this label when you want CI to create a canary release of the current PR label Jun 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an optional NavList.Heading slot to @primer/react’s NavList to provide a single, consistent navigation-region heading (default h2, optionally h3), automatically label the <nav> landmark via aria-labelledby, and derive group heading levels one level deeper for a correct heading outline.

Changes:

  • Add NavList.Heading slot (with visuallyHidden support) and internal context to derive group heading levels.
  • Update NavList.Group / NavList.GroupHeading defaults to depend on the presence/level of NavList.Heading.
  • Add unit tests, Storybook feature stories, docs metadata, and a changeset for a minor release.
Show a summary per file
File Description
packages/react/src/NavList/NavList.tsx Implements NavList.Heading, nav landmark labeling, and derived group heading levels via context.
packages/react/src/NavList/NavList.test.tsx Adds unit coverage for heading rendering, landmark labeling, level derivation, and visually-hidden behavior.
packages/react/src/NavList/NavList.module.css Adds styling for the new heading alignment/spacing.
packages/react/src/NavList/NavList.features.stories.tsx Adds “With Heading” and “With Heading (hidden)” stories to demonstrate the new slot.
packages/react/src/NavList/NavList.docs.json Documents the new NavList.Heading subcomponent and updates group heading docs.
.changeset/navlist-heading-slot.md Declares a minor bump for the new additive API.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

Comment thread packages/react/src/NavList/NavList.tsx Outdated
Comment thread packages/react/src/NavList/NavList.docs.json
Merge intrinsic h2 element props into NavListHeadingProps so consumers can
pass aria-*, data-*, title, and other standard heading attributes without a
type assertion, and document the id prop in NavList.docs.json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@primer-integration

Copy link
Copy Markdown

Integration test results from github/github-ui PR:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

@janmaarten-a11y janmaarten-a11y added integration-tests: passing Changes in this PR do NOT cause breaking changes in gh/gh and removed integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm labels Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Canary Release Apply this label when you want CI to create a canary release of the current PR integration-tests: passing Changes in this PR do NOT cause breaking changes in gh/gh

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants