Skip to content

Fix invalid HTML nesting in ActionList.Heading#8066

Draft
janmaarten-a11y wants to merge 1 commit into
mainfrom
janmaarten-a11y/actionlist-heading-span-fix
Draft

Fix invalid HTML nesting in ActionList.Heading#8066
janmaarten-a11y wants to merge 1 commit into
mainfrom
janmaarten-a11y/actionlist-heading-span-fix

Conversation

@janmaarten-a11y

Copy link
Copy Markdown
Contributor

Background

ActionList.Heading renders the section heading for an ActionList (e.g. an h1h6 that labels the list). Until now it wrapped that heading in a VisuallyHidden component:

<VisuallyHidden isVisible={!visuallyHidden}>
  <HeadingComponent as={as} ...>{children}</HeadingComponent>
</VisuallyHidden>

VisuallyHidden always renders a <span> around its children — it only toggles a CSS class to hide/show. That means every ActionList.Heading ended up as a heading nested inside a <span>, regardless of whether visuallyHidden was set.

That nesting is invalid HTML: a heading (<h1><h6>) is flow content, and a <span> may only contain phrasing content. So we were emitting a <span><h2>…</h2></span> structure that browsers technically have to error-correct. ActionList is used very widely (including indirectly by NavList), so this affected a lot of pages.

This is the same problem — and the same fix — we just landed for NavList.Heading in #8031. This PR brings ActionList.Heading in line.

What changed

Instead of wrapping the heading in a VisuallyHidden span, we apply the visually-hidden CSS class directly to the heading element using clsx:

<HeadingComponent
  as={as}
  className={clsx(className, classes.ActionListHeader, {
    [visuallyHiddenClasses.InternalVisuallyHidden]: visuallyHidden,
  })}
  ...
>
  {children}
</HeadingComponent>

The wrapping <span> is gone, so the heading is now a direct child of the list container — valid HTML.

One detail worth calling out: unlike NavList.Heading, the ActionListHeader class (which provides the heading's margins and the data-list-variant-based spacing) must stay on the heading in all states. So the visually-hidden class is added on top of ActionListHeader only when visuallyHidden is true — they are not mutually exclusive here.

Changed

  • ActionList.Heading no longer wraps its heading in a VisuallyHidden <span>; the visually-hidden styles are applied directly to the heading element.

Removed

  • The internal <span> wrapper around the ActionList.Heading element.

Why this is safe

ActionList is high-traffic, so we checked the things that could plausibly depend on the old structure:

  • Accessible labelling is unaffected. List.tsx and Group.tsx derive aria-labelledby from the heading slot's React props (id ?? headingId), not from the DOM. Removing the wrapper span doesn't change the id wiring.
  • Slot rendering is unaffected. List.tsx renders {slots.heading} directly; only the wrapper element disappears, the heading itself is still emitted.
  • Visible and hidden styling are preserved. ActionListHeader (margins) stays on the heading in every state. When visuallyHidden is set, InternalVisuallyHidden is added (absolute position, 1px clip), matching the previous behaviour.
  • ActionList.GroupHeading is not affected. It uses its own local heading component and only imports the type from Heading.tsx.
  • SSR compatible. This is a pure className change — no new hooks or DOM APIs.
  • No snapshot or e2e test depended on the wrapper span.

Testing & Reviewing

  • Added unit tests in ActionList/Heading.test.tsx covering: the heading is no longer wrapped in a <span>, visuallyHidden applies InternalVisuallyHidden to the heading, and the default (visible) case keeps only ActionListHeader.
  • Ran the ActionList and NavList unit suites together (NavList consumes ActionList.Heading indirectly): all passing.
  • Type-check, full build, and lint/format all pass.

This branch is rebased on top of #8031 (NavList.Heading), since the two changes are closely related.

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

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

Apply the visually-hidden styles directly to the heading element instead
of wrapping it in a VisuallyHidden span. A heading is not valid phrasing
content inside a span, so the wrapper produced invalid HTML for every
ActionList.Heading instance.

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

changeset-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0c52807

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 Patch

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 25, 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.

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

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant