Skip to content

Timeline: Use list semantics for screen reader navigation - #7910

Merged
primer[bot] merged 6 commits into
mainfrom
timeline/list-semantics
Jun 5, 2026
Merged

Timeline: Use list semantics for screen reader navigation#7910
primer[bot] merged 6 commits into
mainfrom
timeline/list-semantics

Conversation

@janmaarten-a11y

@janmaarten-a11yjanmaarten-a11y commented May 29, 2026

Copy link
Copy Markdown
Contributor

Refs github/primer#6679
Closes github/primer#2206
Closes github/primer#3354
Part of the Timeline redesign (github/primer#6654)

Timeline and Timeline.Item currently render as bare <div>s. Screen reader users have no way to know how many events are in the timeline or where they are in the sequence — they have to step through every event one by one.

This was originally flagged by @patrickhlauke in the TetraLogical component audit (April 2023) and tracked for PRC in github/primer#3354.

This changes the rendered elements so assistive technology can announce list size and position (e.g. "item 3 of 12"):

  • Timeline<ol role="list"> (was <div>)
  • Timeline.Item<li> (was <div>)
  • Timeline.Break<li role="presentation"> (was <div>) — excluded from list item count since it's a visual separator, not a timeline event

The explicit role="list" on the <ol> restores list semantics in Safari/VoiceOver, which strips them when list-style: none is applied.

Changelog

Changed

  • Timeline renders as <ol role="list"> instead of <div>. Ref type changes from HTMLDivElement to HTMLOListElement.
  • Timeline.Item renders as <li> instead of <div>. Ref type changes from HTMLDivElement to HTMLLIElement.
  • Timeline.Break renders as <li role="presentation"> instead of <div>. Ref type changes from HTMLDivElement to HTMLLIElement. The role prop is omitted from the type to prevent overriding.

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None

Note on semver: The ref types narrowed (HTMLDivElementHTMLOListElement/HTMLLIElement), which is technically a breaking type change. Marked as minor because: (1) Timeline refs are rarely used in practice, (2) the change is an accessibility improvement, not a feature removal, and (3) all HTML attributes continue to work identically. Open to major if reviewers prefer.

Guidance for paginated timelines

Note

Adding this as reference for future work updating the Timeline component guidance on primer.style

GitHub timelines often contain pagination controls ("Load hidden items") and "show more" buttons interspersed between events. These are not timeline events and should not inflate the list item count. Guidance for consumers:

Wrapping non-event children

Any child of Timeline that is not a timeline event should render as <li role="presentation"> (the same pattern Timeline.Break uses). This keeps it inside the <ol> (valid HTML) while excluding it from the list item count.

Before loading hidden items (81 visible items):

<Timelinearia-label="Pull request timeline">{/* items 1–39 of 81 */}<lirole="presentation"><button>Load 42 hidden items</button></li>{/* items 40–81 of 81 */}</Timeline>

After loading hidden items (123 total items):

<Timelinearia-label="Pull request timeline">{/* items 1–39 of 123 */}{/* newly loaded: items 40–81 of 123 */}{/* items 82–123 of 123 */}</Timeline>

Labelling the timeline

Pass a descriptive aria-label to Timeline (e.g. "Pull request timeline"). Do not include counts in the label — the native <ol> list count updates automatically as items are loaded, so a static count in the label would become stale after "load more" is activated. Screen readers announce the label plus the live item count on entry: "list, Pull request timeline, 81 items".

Focus management after loading more items

When the "load more" button is activated and new items are inserted, the button itself is removed from the DOM. Move focus to the first newly loaded <li> so the user continues from where the gap was. Without explicit focus management, focus falls to <body> and the user loses their place.

Discoverability of load-more controls

Users navigating by list items (e.g. the "i" shortcut in VoiceOver) will skip <li role="presentation"> elements, jumping from item 39 directly to item 40. The load-more button is still reachable via linear browsing (arrow keys) or Tab. This is acceptable because the loaded items are still coherent, but worth noting during AT testing.

Item numbering after loading

After loading hidden items, the native list numbering shifts: what was "item 40 of 81" becomes "item 82 of 123". This is correct — items are now in their true chronological position — but may be disorienting on first encounter. No action needed; this is inherent to how ordered lists work with dynamic content.

Testing & Reviewing

  1. Open any Timeline story in Storybook
  2. Inspect the DOM — confirm <ol role="list"> wraps <li> items
  3. If Timeline.Break is present, confirm it renders as <li role="presentation">
  4. Test with VoiceOver + Safari: navigate to the timeline and confirm it announces as a list with item count
  5. Test with VoiceOver + Chrome or NVDA + Firefox/Chrome: same check

Tracking and consumer adoption

  • Tracking issue: github/primer#6679 — status, rollout plan, and Phase 2 checklist
  • Consumer adoption (Phase 2): github/github-ui#22083 — flips the flag in github-ui once a stable @primer/react ships with the flag
  • Reference branch for consumer fixes:prc-integration-test-for-7910 on github-ui — contains the proven-out set of changes needed in github-ui (story wrappers, ref type updates, empty-list guards, etc.) when the flag is enabled

What this PR ships

Adds the primer_react_timeline_list_semantics feature flag. Default behavior is unchangedTimeline continues to render as <div> / <div> exactly as on main. Opting in via <FeatureFlags flags={{primer_react_timeline_list_semantics: true}}> switches to <ol role="list"> / <li>. See the WithListSemantics story for a visual.

Integration tests should pass cleanly — the flag-off default has no consumer impact. That's the verification we want before merging.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook) — no visual change; purely semantic
  • Changes are SSR compatiblestandard HTML elements, no client-only APIs
  • 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-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5c44739

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

This PR includes changesets to release 1 package
NameType
@primer/reactPatch

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

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

@github-actionsgithub-actionsBot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label May 29, 2026
@janmaarten-a11yjanmaarten-a11y self-assigned this May 29, 2026
@primer

primerBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

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

CopilotAI 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

Improves @primer/react’s Timeline accessibility by restoring true list semantics so assistive tech can announce list size and position (e.g. “item 3 of 12”), supporting the broader Timeline redesign work.

Changes:

  • Timeline now renders an <ol role="list"> and updates its ref/type surface accordingly.
  • Timeline.Item now renders an <li>; Timeline.Break now renders an <li role="presentation"> to avoid inflating the announced item count.
  • Adds CSS resets for list styling and updates/extends unit tests + snapshots for the new markup.
Show a summary per file
FileDescription
packages/react/src/Timeline/Timeline.tsxSwitches Timeline to <ol role="list"> and items/breaks to <li> semantics; updates ref and prop types.
packages/react/src/Timeline/Timeline.module.cssResets list styling (list-style/padding/margin) to preserve existing visuals with <ol>.
packages/react/src/Timeline/tests/Timeline.test.tsxAdds assertions for new rendered elements and required ARIA role semantics.
packages/react/src/Timeline/tests/snapshots/Timeline.test.tsx.snapUpdates snapshot output for <li> rendering.
.changeset/timeline-list-semantics.mdDocuments the semantic change and ref type migrations as a minor release.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 1

Comment threadpackages/react/src/Timeline/Timeline.tsx Outdated
@primer

primerBot commented May 30, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions
github-actionsBottemporarily deployed to storybook-preview-7910 May 30, 2026 00:41 Inactive
@janmaarten-a11y
janmaarten-a11yforce-pushed the timeline/list-semantics branch from e28cff0 to a4a8dc2CompareMay 30, 2026 00:45
@github-actions
github-actionsBot requested a deployment to storybook-preview-7910 May 30, 2026 00:49 Abandoned
@github-actions
github-actionsBottemporarily deployed to storybook-preview-7910 May 30, 2026 00:59 Inactive
@janmaarten-a11y
janmaarten-a11y marked this pull request as ready for review May 30, 2026 01:09
@janmaarten-a11y
janmaarten-a11y requested a review from a team as a code ownerMay 30, 2026 01:09
@janmaarten-a11yjanmaarten-a11y added Canary Release Apply this label when you want CI to create a canary release of the current PR component: Timeline component: TimelineItem labels May 30, 2026
@primer-integration

Copy link
Copy Markdown

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/22069

@janmaarten-a11y

Copy link
Copy Markdown
ContributorAuthor

Companion PR for github-ui: https://github.com/github/github-ui/pull/22083

Updates all RefObject<HTMLDivElement> usages to match the new element types (HTMLOListElement for Timeline, HTMLLIElement for Timeline.Item). 46 files, all mechanical type parameter changes.

@janmaarten-a11y

Copy link
Copy Markdown
ContributorAuthor

Merge sequence:

  1. Integration PR github/github-ui#22069 validates that the canary + ref type fixes compile together
  2. Once CI passes on #22069, this PR (Timeline: Use list semantics for screen reader navigation #7910) can be merged
  3. After the @primer/react release publishes, github/github-ui#22083 is rebased on latest main (picks up the released version) and merged to ship the ref type fixes to dotcom
  4. #22069 is closed (throwaway integration test PR)

@github-actions
github-actionsBottemporarily deployed to storybook-preview-7910 June 2, 2026 20:28 Inactive
@janmaarten-a11yjanmaarten-a11y added Canary Release Apply this label when you want CI to create a canary release of the current PR and removed Canary Release Apply this label when you want CI to create a canary release of the current PR labels Jun 2, 2026
@primerprimer deleted a comment from primer-integrationBotJun 2, 2026
…ics flag
The unconditional ol/li change broke github-ui integration tests (DOM
nesting, ref types, role=list axe issues). Gate the new semantics behind
the primer_react_timeline_list_semantics feature flag so the default
behaviour remains <div>/<div> until consumers opt in.
When the flag is enabled:
- Timeline renders as <ol role="list">
- Timeline.Item renders as <li>
- Timeline.Break renders as <li role="presentation">
Ref types widened to HTMLDivElement | HTMLOListElement (Timeline) and
HTMLDivElement | HTMLLIElement (Item, Break) so consumers using either
state compile.
Tests cover both flag states. A new WithListSemantics story demonstrates
the opt-in behaviour for visual review.
Changeset bumped from minor to patch since the new behaviour is opt-in.
Refs github/primer#6679
@github-actions
github-actionsBottemporarily deployed to storybook-preview-7910 June 3, 2026 22:20 Inactive
@janmaarten-a11yjanmaarten-a11y added Canary Release Apply this label when you want CI to create a canary release of the current PR and removed Canary Release Apply this label when you want CI to create a canary release of the current PR labels Jun 3, 2026
@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!

@adierkens
adierkens enabled auto-merge June 4, 2026 21:43
@primer
primerBot disabled auto-merge June 5, 2026 17:17
@primer
primerBot added this pull request to the merge queueJun 5, 2026
Merged via the queue into main with commit 0c38cfaJun 5, 2026
64 checks passed
@primer
primerBot deleted the timeline/list-semantics branch June 5, 2026 17:26
@primerprimerBot mentioned this pull request Jun 5, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Canary ReleaseApply this label when you want CI to create a canary release of the current PRcomponent: Timelinecomponent: TimelineItemintegration-tests: recommendedThis 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.

4 participants

@janmaarten-a11y@jonrohan@adierkens