Skip to content

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

Description

@h4yfans

Surfaced while fixing epic #987 (memory/CPU audit 2026-08). Not fixed there to keep those PRs surgical.

What's wrong

apps/desktop/src/renderer/src/components/tabs/tab-bar-with-drag.tsx:111-117 — the effect that keeps the active tab visible requests a smooth scroll unconditionally, with no prefers-reduced-motion check:

  useLayoutEffect(() => {
    if (!activeTabId || activeDragItem) return
    const tabEl = scrollRef.current?.querySelector(`[data-tab-id="${CSS.escape(activeTabId)}"]`)
    // scrollIntoView is not implemented in jsdom
    tabEl?.scrollIntoView?.({ inline: 'nearest', block: 'nearest', behavior: 'smooth' })
  }, [activeTabId, regularTabsLength, activeDragItem, canScrollToStart, canScrollToEnd])

The rest of the tab-strip motion in this folder acknowledges the requirement — components/tabs/animations.ts:42 defines tabVariantsReduced (a near-instant opacity-only variant, asserted in tabs-small-components.test.tsx:52) — but nothing gates this call. Per spec the behavior option overrides the CSS scroll-behavior property, so the existing @media (prefers-reduced-motion: reduce) blocks in assets/base.css cannot suppress it either; it has to be handled in JS.

Note tabVariantsReduced is currently exported and unit-tested but has no component consumer, so the reduced-motion path for the tab strip as a whole may be worth checking at the same time.

Why it matters

The repo targets WCAG AA plus reduced-motion (see CLAUDE.md / PRODUCT.md). A user who has asked their OS to reduce motion still gets an animated horizontal slide of the tab strip on every tab activation — one of the most frequently repeated interactions in the app. For motion-sensitive users this is exactly the kind of repeated involuntary movement the preference exists to suppress.

PR #1277 (for #1105) reduced this from up to 3 scheduled smooth scrolls per activation down to 1 by stabilising the effect deps, but did not add the preference gate — so the frequency improved and the a11y gap did not.

Suggested direction

Read the motion preference at call time and pass behavior: 'auto' when reduce is set (matchMedia('(prefers-reduced-motion: reduce)')components/onboarding/use-first-run-tour.ts:33 already does this in the renderer, so there is a precedent to follow rather than a new pattern to invent). Whoever picks this up should check whether a shared hook is warranted given tabVariantsReduced's missing consumer, and confirm the jsdom-guarded optional call still tests cleanly.

Confirmed on

origin/main @ 2ba7f5950 on 2026-08-12.

Metadata

Metadata

Assignees

No one assigned

    Labels

    audit-2026-08-followupSurfaced while fixing the audit-2026-08 epic (#987), not part of the original auditbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions