primitives: Select causing layout shift in external monitor.

Bug report

Current Behavior

Select add --removed-body-scroll-bar-size: 15px on 24inch secondary monitor.
But when I visit on my 13inch, it’s --removed-body-scroll-bar-size: 0px. The same value gets added to the margin-right property of body and it’s causing a layout shift.

Suggested solution

By removing

body {
   margin-right: 15px !important;
}

Software Name(s) Version
Radix Package(s) Select
React n/a
Browser
Assistive tech
Node n/a
npm/yarn
Operating System MacOS

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 5
  • Comments: 48

Commits related to this issue

Most upvoted comments

We can solve this issue, let me debug what’s happening

when you use Radix UI two things happen

  1. overflow: hidden; on the body and
  2. Radix UI will add margin-right: on the body.

Why the margin-right: is being added on the body element?? when there is overflow: hidden; on the body the scrollbar will disappear(width of the scroll bar is zero) and Radix UI will add imaginary scrollbar width in order to prevent layout shift using the margin-right: where the margin-right value will be the width of the scrollbar.

How to Fix this behavior:

  1. First we will add scrollbar-gutter: stable property on the html element(This will have a fixed width for the scrollbar even when the there overflow: hidden is present.) Youtube link to the scrollbar-gutter property and how it works
html {
    scrollbar-gutter: stable;
}
  1. Secondly we will add margin-right: 0px !important; inside body[style]
body {
    margin: 0;
}

body[style] {
    margin-right: 0px !important;
}

I am interested in a reproduction when used correctly.

I don’t believe there is actually any issue with radix-ui’s behavior here. I suspect most people having layout shift issues simply have overflow rules on <html> rather than <body>.

We can solve this issue, let me debug what’s happening

when you use Radix UI two things happen

  1. overflow: hidden; on the body and
  2. Radix UI will add margin-right: on the body.

Why the margin-right: is being added on the body element?? when there is overflow: hidden; on the body the scrollbar will disappear(width of the scroll bar is zero) and Radix UI will add imaginary scrollbar width in order to prevent layout shift using the margin-right: where the margin-right value will be the width of the scrollbar.

How to Fix this behavior:

  1. First we will add scrollbar-gutter: stable property on the html element(This will have a fixed width for the scrollbar even when the there overflow: hidden is present.) Youtube link to the scrollbar-gutter property and how it works
html {
    scrollbar-gutter: stable;
}
  1. Secondly we will add margin-right: 0px !important; inside body[style]
body {
    margin: 0;
}

body[style] {
    margin-right: 0px !important;
}

Well, @Rahul-Palamarthi , I think you fixed it for me 😄 The missing piece for my use case was the scrollbar-gutter, apparently. A fine bit of CSS dark magic (thx for the link, btw).

I modified you snippet since I use auto margins with Tailwind (container mx-auto) and everything works great:

html {
  scrollbar-gutter: stable;
}

body[style] {
  margin: 0 auto !important;
}

Thank you so much!

With 100% width position: fixed elements (e.g. a fixed header) the margin applied to body by react-remove-scroll causes a visual shift in that element as the page margin doesn’t affect the fixed element. That causes a very obvious shift when the select is open and radix disables scroll via react-remove-scroll.

Thankfully react-remove-scroll adds a CSS var --removed-body-scroll-bar-size that you can use to manually apply the offset that it adds as a margin to the page onto the width of your fixed element. i.e. something like: width: calc(100% - var(--removed-body-scroll-bar-size, 0px)); or w-[calc(100%-var(--removed-body-scroll-bar-size,0px))] for Tailwind users

For reproduction, on Mac at least, it helps to turn set show scroll bars in General preferences to “always”:

Screenshot 2023-05-13 at 00 10 12

My best solution was returning to native HTML <select>. It works great 😂

I’m getting this behaviour too when using the DropdownMenu component

In my case @kiastorm, I was facing the same issue on the DropdownMenu component, but I fixed that by removing a custom css prop I’ve added at html, body tags. which was overflow-x: hidden

Screenshot 2023-03-07 at 15 20 14

After that, it worked again!

This seems to happen to avoid a layout shift on body when the scrollbar is removed by activating a popover. What worked for me is adding:

body {
  overflow-y: scroll;
}

This makes the scrollbar always visible which prevents a layout shift on pages where there is no overflow.

html body {

overflow-y: auto !important; margin-right: 0 !important;

}

It’s broken down here (make sure you’re aware of current browser support before going this route, hopefully Safari adds support soon) and here and there are viable solutions in both of those comments for anyone interested. If you actually want the scrollbar to be visible when the select is open then I imagine that’s not something that anything in this issue would help with.

Bit of a re-breakdown if it helps anyone, though there is loads of information already here, this is just an attempt:

The issue isn’t really about whether the scroll bar is visible or not, it’s relating to layout shift caused by the implementation of preventing a shift due to the scrollbar taking viewport space before the select is opened, and then maintaining the previously occupied scrollbar space when it is no longer showing after the select is opened due to overflow: hidden on the body. As there is a scrollbar and then there isn’t and radix’s implementation of dealing with that via [react-remove-scroll] doesn’t affect fixed elements, because the margin on the body it adds doesn’t affect those elements, there is a visible layout shift:

2023-12-20 12 25 03

However, you’ll only see that issue if you have scrollbars set to “Always” in MacOS (see https://github.com/radix-ui/primitives/issues/1925#issuecomment-1546419319 for reference of the setting I’m talking about). That header is using fixed positioning and so it’s affected. This happens because the viewport width actually changes when the scrollbar goes.

If you have the OS level setting set to “When scrolling” (or they aren’t showing based on the automatic setting) then you won’t see the issue in the first place. This is because with that setting, the scrollbar sits on top of the page, not ever affecting the viewport width.

2023-12-20 12 33 06

That screenshot shows my original proposed solution, but scrollbar-gutter does also help, I just couldn’t use it when I had to deal with this issue due to lack of Safari support for that property.

Alright, I think I get the whole picture, now.

  • Since expanding a <select> element natively disables the scroll bar, Radix hides it entirely for accessibility purposes.
  • Hiding the scroll bar leaves more room for the page, so CSS instructions like margin: auto get very confused.

I find this behaviour very intrusive. I’d love to have a way to disable it entirely rather than having to fix it with variable overriding. Removing the scroll bar is simply not a behaviour I want on my UI.

Hello, I fixed this by adding w-screen (width: 100vw) class to fixed component. I had a fixed header so when I opened the Dialog component, header shifts.

Why are you adding a stylesheet with marginRight:19px !important? image image

None of the above mentioned fix work for me.

OK so I have overridden the react-remove-scroll used in radix-ui components, react-dialog and react-popover to use the latest “2.5.9” and the issues have gone so far from testing, this is to do with using layered components using these components, i.e. a dropdown menu on a dialog etc. This is what I placed in my package.json…

"overrides": {
"@radix-ui/react-dialog": {
      "react-remove-scroll": "2.5.9"
    },
    "@radix-ui/react-popover": {
      "react-remove-scroll": "2.5.9"
    }
}

for me just increasing the css specificity to the body selector worked

html body {
  margin-right: 0px !important;
}

Unfortunately, there’s no way to disable it by setting up a config param, that’s why this issue for as well. Would be really good if they’d allow us to disable that behavior

It’s not the best approach, but putting the following line will override the inline-style props on the css:

body[style] { margin: 0 !important;}

I’m seeing the same behavior, any work around? My body doesn’t habe any classes. I have tried using position popper and items-aligned and same issue always

CleanShot 2023-04-01 at 10 57 02

Update: I had to add a (margin-right: 0 !important) to the body and seems to work.

Best worked solutions for me for shadcn and macos tested:

html {
  scrollbar-gutter: stable;
}

body[style] {
  margin: 0 auto !important;
}

body[data-scroll-locked] {
  overflow: hidden;
  position: fixed;
  width: 100%;
}
html {
  scrollbar-gutter: stable;
}

body[style] {
  margin: 0 auto !important;
}
```@NicoToff Thanks, this is the only solution that worked for me

@NicoToff I don’t personally recommend you using the important option, since this adds “!important” rule on each tailwindcss style, this might cause numerous issues if you try to add some custom css that can override tailwind. I suggest you disabling the inline margin property that radix adds to the container.

This bug is caused by a margin, --removed-body-scroll-bar-size added by radix or its dependency(possibly react-remove-scroll). this breaks the page layout inchrome for some cases. how to reproduce.

Add a <div style="position: fixed; width: 100%"> inside body and add a radix select. open the select and fixed element will move as you open-close select component.

I can try to debug it more and open a PR. 😃

Worked for me adding margin: 0 !important; on the body! Also worked with overflow: hidden !important. But why does it add this margin-right: 15px to the body in the first place? 🤔