Timeline: Use list semantics for screen reader navigation#7910
Timeline: Use list semantics for screen reader navigation#7910janmaarten-a11y wants to merge 3 commits into
Conversation
Timeline renders as <ol role="list">, Timeline.Item as <li>, and Timeline.Break as <li role="presentation">. Gives screen reader users list navigation with item count and position. Explicit role="list" restores semantics in Safari/VoiceOver which strips them when list-style: none is applied (WebKit intentional behaviour). Refs github/primer#6679
🦋 Changeset detectedLatest commit: a4a8dc2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
There was a problem hiding this comment.
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:
Timelinenow renders an<ol role="list">and updates its ref/type surface accordingly.Timeline.Itemnow renders an<li>;Timeline.Breaknow 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
| File | Description |
|---|---|
| packages/react/src/Timeline/Timeline.tsx | Switches Timeline to <ol role="list"> and items/breaks to <li> semantics; updates ref and prop types. |
| packages/react/src/Timeline/Timeline.module.css | Resets list styling (list-style/padding/margin) to preserve existing visuals with <ol>. |
| packages/react/src/Timeline/tests/Timeline.test.tsx | Adds assertions for new rendered elements and required ARIA role semantics. |
| packages/react/src/Timeline/tests/snapshots/Timeline.test.tsx.snap | Updates snapshot output for <li> rendering. |
| .changeset/timeline-list-semantics.md | Documents the semantic change and ref type migrations as a minor release. |
Copilot's findings
- Files reviewed: 5/5 changed files
- Comments generated: 1
Move role="list" after the props spread so it cannot be overridden, and omit role from TimelineProps to prevent passing it at the type level. Matches the pattern used for Timeline.Break's role="presentation".
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
The rule flags role="list" on <ol> as redundant, but it is required to restore list semantics in Safari/VoiceOver when list-style: none is applied.
e28cff0 to
a4a8dc2
Compare
|
A successful canary CI run (i.e., a valid canary version published via the Next steps:
For more details, see this workflow run. |
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"):
<ol role="list">(was<div>)<li>(was<div>)<li role="presentation">(was<div>) — excluded from list item count since it's a visual separator, not a timeline eventThe explicit
role="list"on the<ol>restores list semantics in Safari/VoiceOver, which strips them whenlist-style: noneis applied.Changelog
Changed
Timelinerenders as<ol role="list">instead of<div>. Ref type changes fromHTMLDivElementtoHTMLOListElement.Timeline.Itemrenders as<li>instead of<div>. Ref type changes fromHTMLDivElementtoHTMLLIElement.Timeline.Breakrenders as<li role="presentation">instead of<div>. Ref type changes fromHTMLDivElementtoHTMLLIElement. Theroleprop is omitted from the type to prevent overriding.Rollout strategy
Note on semver: The ref types narrowed (
HTMLDivElement→HTMLOListElement/HTMLLIElement), which is technically a breaking type change. Marked asminorbecause: (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 tomajorif 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
Timelinethat is not a timeline event should render as<li role="presentation">(the same patternTimeline.Breakuses). This keeps it inside the<ol>(valid HTML) while excluding it from the list item count.Before loading hidden items (81 visible items):
After loading hidden items (123 total items):
Labelling the timeline
Pass a descriptive
aria-labeltoTimeline(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
<ol role="list">wraps<li>items<li role="presentation">Merge checklist