fix: stop sticky group header jitter while scrolling#49
Conversation
…tter The sticky group header overlay was rendered through rc-virtual-list's extraRender, which places it inside the natively-scrolled holder. To stay pinned it recomputed top = scrollTop - offsetY on every React render. The content scrolls via the holder's native scrollTop (synchronous) while the overlay's compensating top lands through a React commit that can paint one frame later, so on transient frames the header lags the scroll by one step and snaps back - the jitter. Render the overlay into the outer, non-scrolled container via Portal and make top viewport-relative (0 within a group, negative only when the next header pushes it up). Within a group there is no per-frame compensation, so nothing can lag. overflow: hidden on the list root clips the pushed-up header to the viewport so it cannot bleed over content above the list.
Walkthrough为 Changes粘性表头 Portal 渲染重构
估算代码审查工作量🎯 3 (Moderate) | ⏱️ ~20 minutes 可能相关的 PR
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #49 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 8 8
Lines 207 209 +2
Branches 61 63 +2
=========================================
+ Hits 207 209 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request refactors the sticky group header rendering in the virtual list component to use a Portal targeted at the list container, simplifying the top offset calculation. The tests have been updated to support this portal-based rendering. The review feedback suggests several improvements: defensively handling potential undefined returns from getSize to prevent runtime crashes, wrapping the portaled header in a localized clipping container instead of applying overflow: hidden globally to avoid clipping list item children, and explicitly unmounting components in tests before removing custom DOM containers to prevent memory leaks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR eliminates sticky grouped header “jitter” during scroll by moving the sticky header overlay out of the natively scrolled container and into the outer list container via a portal, so the header can stay pinned without per-frame scroll compensation.
Changes:
- Render the sticky group header overlay using
@rc-component/portalinto the list’s outer container and computetopviewport-relatively. - Extend
useStickyGroupHeaderto accept the virtual list ref (listRef) so it can target the correct portal container. - Add
overflow: hiddenon the list root to clip the pushed-up sticky header at the top boundary, and update unit tests to match the new positioning contract.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/hooks.test.tsx | Updates useStickyGroupHeader tests to validate the new portal-based rendering and the new top contract. |
| src/VirtualList/useStickyGroupHeader.tsx | Ports the sticky header overlay into the outer container and switches to viewport-relative top math to avoid scroll/render desync. |
| src/VirtualList/index.tsx | Passes the virtual list ref into useStickyGroupHeader so it can locate the portal container. |
| assets/index.less | Clips pushed headers within the list viewport by applying overflow: hidden on the list root. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Fixes a subtle jitter of the sticky group header when scrolling up and down within a group (reproducible in the
groupdemo).Root cause
The sticky header overlay is rendered through rc-virtual-list's
extraRender, which places it inside the natively-scrolled holder (thetranslateY(offsetY)container). To keep it visually pinned, listy recomputed itstopasscrollTop - offsetYon every React render.But the two positioning channels are not in sync:
scrollTop— applied synchronously, takes effect the same frame.topis applied through a React render/commit, which can paint one frame later.On frames where the native scroll has already advanced but the re-render carrying the new
tophasn't committed yet, the header is left under-compensated by exactly one scroll step and then snaps back the next frame. The list items, driven directly by native scroll, move correctly — so only the header appears to shake.Per-frame measurement before the fix (fresh downward scroll) caught transient frames where the header's on-screen position was a full scroll step off its intended
styleTop(e.g. rendered at-16pxwhile it should be0).Fix
Render the overlay into the outer, non-scrolled list container (
listRef.nativeElement) viaPortal, and maketopviewport-relative:0— there is no per-frame scroll compensation, so nothing can lag → no jitter.topgoes negative) near a boundary, so the push animation is preserved.overflow: hiddenon the list root clips it back to the list viewport (the natural "slide out the top" behavior).Verification
Per-frame sampling in a foreground browser at full frame rate:
styleTopon every frame — the desync channel is eliminated by construction.useStickyGroupHeadertests updated for the new viewport-relative contract.Before / After
Before (header jitters while scrolling within a group)
After (header stays pinned; push animation clipped to the list)
🤖 Generated with Claude Code
Summary by CodeRabbit