Skip to content

fix(a11y): center the active tab by scrolling the strip, not the page - #673

Closed
bfs2021 wants to merge 2 commits into
richshaw:mainfrom
bfs2021:fix/644-tabs-scroll
Closed

bfs2021 wants to merge 2 commits into
richshaw:mainfrom
bfs2021:fix/644-tabs-scroll

Conversation

@bfs2021

@bfs2021 bfs2021 commented Oct 26, 2026

Copy link
Copy Markdown

Problem

On a signup's Build, Responses and Settings tabs, the first Tab after the page loads skips the whole header and lands on the next tab (Fixes #644).

Root cause

TabsNav centered the active tab with scrollIntoView. Besides scrolling the strip, scrollIntoView also moves Chrome's sequential focus navigation starting position past the active tab — so the first Tab press started from the tab itself and went to whatever followed it in DOM order, skipping the header links.

Fix

Scroll the tablist element directly instead: tablist.scrollLeft = el.offsetLeft - (tablist.clientWidth - el.offsetWidth) / 2. Setting scrollLeft on the strip scrolls only the strip and never touches the focus-navigation starting position, and it keeps the same centering behavior. Plain scrollLeft (rather than scrollTo) also runs under jsdom, which implements neither scroll API.

Testing

  • Added src/components/signup/TabsNav.test.tsx:
    • switching tabs scrolls the strip itself to the centered offset (240 at the stubbed geometry) and never the page;
    • scrollIntoView is never called.
  • pnpm lint and pnpm typecheck pass.
  • Full npx vitest run: 1778 passed, 3 failed — the same 3 fail on unmodified main in this environment (env.test.ts .env.example golden, worker-render golden, magic-compose golden), so they are pre-existing and unrelated.
  • pnpm knip could not run locally (OOM at baseline too), so it is left to CI.

bfs2021 and others added 2 commits October 26, 2026 14:12
scrollIntoView moves Chrome's sequential focus navigation starting point
past the active tab, so the first Tab after load skipped the whole header
and landed on the next tab. Scrolling the tablist itself (plain
scrollLeft, also readable under jsdom) keeps the tab in view without
touching that starting position.
@richshaw

Copy link
Copy Markdown
Owner

Thanks so much for picking this up, and for the clear write-up and tests. We'd already opened richshaw/OpenSignup#672 for richshaw/OpenSignup#644, so we’re going to land that one and close this. One thing for next time, offsetLeft is measured from the nearest positioned ancestor (here the page body), not the tab strip, so the centring needs - strip.offsetLeft. It only works on phones because the strip happens to start at x=0 there.

@richshaw richshaw closed this Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A signup page's first Tab skips the header and lands on the next tab

2 participants