Skip to content

fix(ui): Prevent body scroll when CompactSelect menu is open#108191

Open
JonasBa wants to merge 8 commits intomasterfrom
jb/compactselect/page-scroll
Open

fix(ui): Prevent body scroll when CompactSelect menu is open#108191
JonasBa wants to merge 8 commits intomasterfrom
jb/compactselect/page-scroll

Conversation

@JonasBa
Copy link
Member

@JonasBa JonasBa commented Feb 12, 2026

When a CompactSelect menu is open, the body overflow is now set to 'hidden' to prevent scroll propagation to the page. The previous overflow value is restored when the menu closes.

This follows the same pattern used in the global drawer component to ensure proper overlay behavior and prevent background scrolling.

When a CompactSelect menu is open, the body overflow is now set to 'hidden'
to prevent scroll propagation to the page. The previous overflow value is
restored when the menu closes.

This follows the same pattern used in the global drawer component to ensure
proper overlay behavior and prevent background scrolling.
@JonasBa JonasBa requested a review from a team as a code owner February 12, 2026 23:56
@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Feb 12, 2026
Comment on lines 324 to 328
// Prevent body scroll while the select menu is open
previousOverflowRef.current ??= document.body.style.overflow;
document.body.style.overflow = 'hidden';

// Force a overlay update, as sometimes the overlay is misaligned when opened
Copy link
Member Author

Choose a reason for hiding this comment

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

@TkDodo there are now two places where we use this, so it may be worth consolidating if we have another instance of it

Addresses PR feedback by creating a shared utility that coordinates body
overflow locking across CompactSelect and GlobalDrawer components.

Key improvements:
- Prevents race conditions when multiple overlays are active simultaneously
- Adds cleanup on CompactSelect unmount to prevent permanent scroll lock
- Uses reference counting to ensure overflow is only restored when all locks are released
- Eliminates code duplication between components

Fixes issues where body scroll could remain locked if:
1. CompactSelect unmounted while menu was open
2. Multiple components closed in unexpected order
Wrap the return object in useMemo to prevent unnecessary re-renders in
components that use this hook.
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.


// On close
onClose?.();
scrollLock.release();
Copy link
Contributor

Choose a reason for hiding this comment

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

Deferred scroll lock acquire can orphan permanent body lock

Medium Severity

scrollLock.acquire() and scrollLock.release() are called inside nextFrameCallback (a requestAnimationFrame wrapper), but the useScrollLock cleanup releases synchronously on unmount. If the component unmounts between the rAF being scheduled and firing (e.g., the onOpenChange prop callback triggers a parent state change that removes the CompactSelect), the deferred acquire will execute after cleanup, setting document.body.style.overflow to 'hidden' on an orphaned Lock instance with no future cleanup — making the page permanently unscrollable. Unlike the GlobalDrawer usage, which calls acquire/release synchronously, this deferred path has no cancellation guard.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, this is legit, but can also be a problem for other calls

@cursoragent store the raf id in a react ref, and cancel the raf from the cleanup callback


if (this.acquiredBy.size === 0 && this.initialOverflow !== null) {
this.container.style.overflow = this.initialOverflow;
if (this.container === document.body) {
Copy link
Member Author

Choose a reason for hiding this comment

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

We update the scrollbar gutter setting so that we don't get layout shift. We do it conditionally so that in the views without scrollbars, there is no reserved white area where the scrollbar would otherwise be

Copy link
Collaborator

@TkDodo TkDodo left a comment

Choose a reason for hiding this comment

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

approving, but please move the util to scraps before merging

JonasBa and others added 2 commits February 13, 2026 04:44
Moves useScrollLock utility and its test file from static/app/utils/ to static/app/components/core/ to better organize core UI utilities alongside other core components.
@JonasBa
Copy link
Member Author

JonasBa commented Feb 23, 2026

I'm holding off on merging until I figure out if we want this to have a proper backdrop component or not so that we can also prevent clicks. Right now it is quite wonky in the sense that hovering other components displays their interaction states, but then we also block the page. I'm not happy sold on this being the intermediary state

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants