Skip to content

fix: stop the whole page scrolling away during playback - #9

Merged
renrenmimi merged 1 commit into
mainfrom
fix/page-scrolls-during-playback
Sep 3, 2026
Merged

renrenmimi merged 1 commit into
mainfrom
fix/page-scrolls-during-playback

Conversation

@renrenmimi

Copy link
Copy Markdown
Owner

Reported from production: open the demo, press Play, and the entire
application could be scrolled up off the screen
, revealing a second screen of
blank page underneath. It got worse the longer playback ran.

Reproduced

Against a production build, measuring document.scrollHeight against the
viewport through a playback:

Window Document taller than viewport by A real wheel event
1440×900 528px scrolled the page 528px
1440×780 512px scrolled 512px
1280×720 538px scrolled 538px
1512×830 496px scrolled 496px

Every laptop-shaped window, not an edge case.

Cause

The visually-hidden utility — the usual sr-only recipe:

.visually-hidden {
  position: absolute;   /* but no top / left */
  width: 1px;
  height: 1px;
  margin: -1px;
  clip-path: inset(50%);
}

With no top/left the box sits at its static position. That is harmless on
a page that scrolls anyway, and wrong in a fixed-height shell, because the tree
emits one of these labels per row ("added in this commit") inside a virtualised
list — so a row a thousand pixels down the list put its 1px box a thousand
pixels down the page. Playback made it grow because the tree grows.

.shell has overflow: hidden and is exactly 100dvh, so it looks like it
should have contained them. It did not, and this is the part worth remembering:
an absolutely positioned box is only clipped by an ancestor's overflow when
that ancestor is itself a containing block
, and .shell was position: static. So the labels' containing block was the initial containing block — the
page — and they extended it.

A bisection confirmed it: hiding each candidate and re-measuring pointed at the
visually-hidden spans, whose deepest bottom edge was 1428px in a 900px window.

Fix

Both ends, because either alone would leave the trap set for the next person:

  • .visually-hidden is pinned to its containing block's origin (top: 0; left: 0), so its box can never be below the fold whatever the flow around it
    does. Nothing using the class is focusable or visible — an h3, a caption
    and two spans — so position has no other consequence.
  • .shell is position: relative, so the overflow: hidden it already
    carried actually clips. That is the right thing for an element whose job is to
    clip, independently of this bug.

Verified

  • 0px of vertical overflow through a full sixteen-commit playback, in both
    sub-views, with a diff open, in Compare and in Insights, at 1440×900, 1280×720
    and 1024×768.
  • A wheel event over the right-hand edge — where the blank space appeared —
    moves the page 0px.
  • Horizontal overflow still 0 everywhere.
  • The hidden labels are still in the accessibility tree: tree rows still read
    "~ index.ts 167 B modified in this commit", and the five speed buttons are
    still named through their hidden speed suffix.
  • Short and narrow windows still scroll, which is deliberate: below 900px
    wide or 620px tall the pinned frame is given up rather than forcing the
    application into a viewport it does not fit. 390×844 still scrolls 242px,
    360×800 still scrolls 286px.

Three tests added. The two that assert the page cannot scroll fail on the old
CSS and pass on the new
— checked by reverting the fix and re-running, which is
the only reason to trust them. The third asserts the narrow layout still scrolls,
so the fix cannot quietly turn into the opposite bug.

Check Result
npm run typecheck clean
npm run lint clean
npm test 792 passed
npm run build clean
npx playwright test 254 passed (was 248)

🤖 Generated with Claude Code

Reported from production: open the demo, press Play, and the entire application
could be scrolled up off the screen, revealing a second screen of blank page
underneath. It got worse the longer playback ran.

Measured before the fix, on a production build: the document was 528px taller
than the viewport at 1440x900, and a real wheel event scrolled it. Reproduced at
1440x780, 1280x720 and 1512x830 too — every laptop-shaped window.

The cause is the `visually-hidden` utility. It is the usual `sr-only` recipe:
`position: absolute`, one pixel, clipped to nothing. What the recipe leaves out
is `top` and `left`, so the box sits at its *static* position. That is harmless
on a page that scrolls anyway, and wrong in a fixed-height shell, because the
tree emits one of these labels per row ("added in this commit") inside a
virtualised list — so a row a thousand pixels down the list put its box a
thousand pixels down the page. Playback made it grow because the tree grows.

`.shell` has `overflow: hidden` and is exactly `100dvh`, so it looks like it
should have contained them. It did not: an absolutely positioned box is only
clipped by an ancestor's overflow when that ancestor is itself a containing
block, and `.shell` was `position: static`.

Fixed at both ends:

- `.visually-hidden` is pinned to its containing block's origin, so its box can
  never be below the fold whatever the flow around it does. Nothing using the
  class is focusable or visible, so position has no other consequence.
- `.shell` is `position: relative`, so the `overflow: hidden` it already carries
  actually clips — the right thing for an element whose job is to clip.

Verified: 0px of vertical overflow through a full sixteen-commit playback, in
every sub-view, in Compare and Insights, at 1440x900, 1280x720 and 1024x768, and
a wheel event over the right-hand edge moves nothing. The hidden labels are
still in the accessibility tree — tree rows still read "modified in this commit"
and the speed buttons are still named through their hidden suffix.

Short and narrow windows still scroll, which is deliberate: below 900px wide or
620px tall the pinned frame is given up rather than forcing the application into
a viewport it does not fit. There is a test for that too, so the fix cannot
quietly turn into a different bug.

The two new tests fail on the old CSS and pass on the new, which is the only
reason to trust them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 3, 2026 20:32
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
repo-time-machine Ready Ready Preview Sep 3, 2026 8:32pm UTC

@renrenmimi
renrenmimi merged commit 7c15103 into main Sep 3, 2026
4 checks passed
@renrenmimi
renrenmimi deleted the fix/page-scrolls-during-playback branch September 3, 2026 20:37

This branch was successfully deployed

1 active deployment
Preview — b31e0f51 Deployed Sep 3, 2026 by vercel[bot]
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.

2 participants