Skip to content

fix(tabs): gate tab-strip scrolling on prefers-reduced-motion - #1304

Merged
h4yfans merged 2 commits into
mainfrom
tab-scroll-reduced-motion-gate
Aug 12, 2026
Merged

fix(tabs): gate tab-strip scrolling on prefers-reduced-motion#1304
h4yfans merged 2 commits into
mainfrom
tab-scroll-reduced-motion-gate

Conversation

@h4yfans

@h4yfans h4yfans commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #1290. Part of epic #987 (memory/CPU audit 2026-08).

What was wrong

apps/desktop/src/renderer/src/components/tabs/tab-bar-with-drag.tsx:138 — the effect that keeps the active tab visible always asked for an animated scroll:

tabEl.scrollIntoView?.({ inline: 'nearest', block: 'nearest', behavior: 'smooth' })

Per spec the behavior member of a scroll options dictionary overrides the CSS scroll-behavior property, so none of the @media (prefers-reduced-motion: reduce) blocks in assets/base.css can suppress it — and the global one there only clamps animation-duration / transition-duration, which do not apply to scrolling at all. Result: a user who asked their OS to reduce motion still got an animated horizontal slide of the tab strip on every single tab activation, one of the most repeated interactions in the app.

The same file had two more ungated motion paths on the same strip:

  • :153 the chevron buttons — el.scrollBy({ left: distance * sign, behavior: 'smooth' }), same override, same reason.
  • :222 the scroll-smooth class. handleWheel scrolls by assigning scrollLeft directly, and the CSS property animates that assignment — there is no options object to gate, so this one genuinely does need the CSS side.

PR #1277 cut this from up to 3 smooth scrolls per activation to 1 by stabilising the effect deps; it did not add a preference gate, so the frequency improved and the a11y gap did not.

What changed

One module-scope helper, read at call time:

const scrollBehavior = (): ScrollBehavior =>
  window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'

applied to both scroll calls, plus motion-reduce:scroll-auto alongside scroll-smooth for the wheel path. Reduce on → 'auto' (the tab still gets scrolled into view, it just arrives instantly); reduce off → 'smooth', byte-for-byte the previous behaviour.

Read at call time rather than via a subscription (matchMedia(...) matches the existing renderer precedent in components/onboarding/use-first-run-tour.ts:41): nothing renders from the value, so a hook would only add a subscription and a re-render for a value used imperatively. It also means a mid-session change to the OS setting is honoured by the very next scroll — covered by a test that flips the preference without remounting.

Deliberately not done:

  • tabVariantsReduced was not wired up. The issue flagged it as having no component consumer — correct, but so does tabVariants itself: nothing in the tab strip uses motion/react at all. Wiring the reduced variant up means first introducing framer-motion enter/exit animation to the tabs, which is a redesign, not a gate. Worth its own issue; it is not a prerequisite for this fix.
  • tab-bar.tsx:69,74 and tab-bar-with-overflow.tsx:131,151 carry the identical ungated behavior: 'smooth'. Left alone: TabBar / TabBarWithOverflow have no consumer outside components/tabs/index.ts and their own tests — the live surface is TabBarWithDrag. Flagging rather than touching, since "are these dead?" is a separate question from this one.

How it was proven

apps/desktop/src/renderer/src/components/tabs/tab-bar-scroll.test.tsx, four new cases: the smooth default, the reduced-motion branch (asserting behavior: 'auto' and that the correct tab element is still scrolled into view, on mount and on re-activation), the chevron gate with the preference flipped mid-session, and the CSS variant for wheel scrolling.

Same test file against the pre-fix tab-bar-with-drag.tsx (git checkout origin/main -- <source>, test file untouched):

before:  Tests  3 failed | 3 passed (6)
after:   Tests  6 passed (6)

The 3 pre-existing cases pass in both runs — the gate does not change the un-reduced path or the once-per-activation behaviour from #1277.

Verification

$ ./node_modules/.bin/vitest run --config apps/desktop/config/vitest.config.ts \
    --project renderer src/renderer/src/components/tabs
  Test Files  8 passed (8)
       Tests  45 passed (45)

$ pnpm --filter @memry/desktop typecheck:web        # clean, no output
$ ./node_modules/.bin/eslint <changed source>        # 0 errors
$ git diff --check                                   # clean
$ pnpm docs:impact --base origin/main --strict       # docs impact: covered
$ pnpm docs:build                                    # build complete in 6.59s

Docs: apps/docs/src/user-guide/tabs-split-view.md picks up the reduced-motion behaviour in the existing tab-bar scrolling paragraph.

Backward compatibility

Renderer-only, presentation-only: no schema, IPC contract, settings, sync or file-format surface is touched, and with reduce-motion off the emitted scroll options are identical to before.

@h4yfans
h4yfans marked this pull request as ready for review August 12, 2026 14:52
@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation test labels Aug 12, 2026
@github-actions

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit fb1b55b.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@h4yfans
h4yfans merged commit f79f3f7 into main Aug 12, 2026
17 of 18 checks passed
@h4yfans
h4yfans deleted the tab-scroll-reduced-motion-gate branch August 12, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[LOW][a11y] Tab activation scrolls with behavior: smooth with no prefers-reduced-motion gate

1 participant