[Regression on closed #366] all six route-level loading.tsx skeletons have lost their ARIA live region — route changes are silent for screen readers
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 72/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- next.js, react, tailwindcss, typescript
- Domain
- accessibility, frontend
Research direction
Read the six route boundaries at src/app/loading.tsx, src/app/dashboard/loading.tsx, src/app/issues/loading.tsx, src/app/issues/[id]/loading.tsx, src/app/milestones/loading.tsx, and src/app/reputation/[handle]/loading.tsx. Compare their accessibility pattern with src/app/auth/callback/page.tsx and check the loading-boundary test file mentioned for #667. Done when each boundary announces loading accessibly, supports reduced motion, and has test coverage for role="status".
Written by the indexing model from the issue text.
Description
Location
All six route-level loading boundaries, none of which carries a live region:
src/app/loading.tsx:5src/app/dashboard/loading.tsx:5src/app/issues/loading.tsx:3src/app/issues/[id]/loading.tsxsrc/app/milestones/loading.tsxsrc/app/reputation/[handle]/loading.tsx
Problem
Each file returns a purely visual skeleton — a container with animate-pulse and a grid of grey placeholder <div>s — and nothing else:
export default function Loading() {
return (
<div className="mx-auto max-w-6xl px-6 py-12 animate-pulse">
<div className="h-8 w-48 rounded-lg bg-slate-200 dark:bg-slate-800" />
...
grep -rn 'role=\|aria-' src/app/**/loading.tsx returns zero matches across all six files. There is no role="status", no aria-live, no aria-busy, and no visually-hidden text naming what is loading. Nothing higher in the tree supplies one either: src/app/layout.tsx wraps {children} in a bare <main id="main-content"> with no live region, and Next.js renders a loading.tsx as the Suspense fallback in that same slot.
This is precisely what closed issue #366 was about — "None of the five route-level loading.tsx skeleton screens have an ARIA live-region/status role" — and the fix is no longer present in any of the files, including the sixth one added since. For contrast, the pattern is still correct elsewhere in the app: src/app/auth/callback/page.tsx:15 and src/app/auth/callback/CallbackClient.tsx:154 both use <p role="status" aria-live="polite">, so the convention exists and these six files diverge from it.
Why it matters
Client-side navigation in the App Router does not reload the document, so a screen reader gets no page-load announcement. The skeleton is the only indication that anything is happening, and it is announced as nothing at all — a sighted user sees a shimmer, a screen-reader user hears silence between activating a link and the new page's content appearing. On /issues/[id] and /reputation/[handle], where the fetch can take a full second against a live backend, the user has no way to tell whether their activation registered, and is likely to activate the link again.
animate-pulse also runs unconditionally, so the skeleton ignores prefers-reduced-motion — a smaller instance of what closed issue #258 covered app-wide.
Reproduction
npm run dev, then enable VoiceOver (macOS) or NVDA (Windows).- From
/issues, activate a bounty card link. - The skeleton from
src/app/issues/[id]/loading.tsxrenders. Nothing is announced; the screen reader stays silent until the resolved page content lands and focus happens to be moved there. curl -s http://localhost:3000/issues | grep -c 'role="status"'against a slow backend returns 0 for the skeleton markup.
Suggested fix
Give each skeleton a polite live region with an accessible name, and mark the region busy while it is up. For example, in each loading.tsx:
<div
role="status"
aria-live="polite"
aria-busy="true"
className="mx-auto max-w-6xl px-6 py-12 animate-pulse motion-reduce:animate-none"
>
<span className="sr-only">Loading bounties…</span>
{/* existing placeholder divs, with aria-hidden="true" on the wrapper that holds them */}
Because all six files repeat the same structure, the cleanest version is a single shared <RouteSkeleton label="…"> component in src/components/ui/ that owns the live region, the sr-only label and the motion-reduce:animate-none class, with each loading.tsx passing only its own label and placeholder shape. That also stops the next new route from reintroducing the gap.
Add an assertion to the loading-boundary test file (added for closed #667) that every loading.tsx default export renders an element with role="status", so this cannot silently regress a third time.
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 94
- Avg merge
- 1h 2m
- Merged PRs (30d)
- 25
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from MergeFi/frontend
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug testing
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
accessibility bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
MergeFi/frontend#835 · 1 comment ·
Maintainers usually reply within 1 day
All issues in MergeFi/frontend
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
zapcooking/frontend#946 ·
Maintainers usually reply within 1 day
-
enhancement good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
WiliamRosa/cosmos-semantic-platform#40 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
liberation-data/drivine#733 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
makecindy/cindy#10062 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day