Skip to content

feat(RAC): expose --page-width and --visual-viewport-width from Modal#9318

Merged
snowystinger merged 2 commits intoadobe:mainfrom
lixiaoyan:patch-6
Mar 17, 2026
Merged

feat(RAC): expose --page-width and --visual-viewport-width from Modal#9318
snowystinger merged 2 commits intoadobe:mainfrom
lixiaoyan:patch-6

Conversation

@lixiaoyan
Copy link
Copy Markdown
Contributor

@lixiaoyan lixiaoyan commented Dec 10, 2025

This allows us to properly place a dialog in a horizontally scrolled page (document.scrollingElement.scrollLeft !== 0).

<ModalOverlay className="absolute top-0 left-0 h-(--page-height) w-(--page-width)">
  <Modal className="grid place-items-center sticky top-0 left-0 h-(--visual-viewport-height) w-(--visual-viewport-width)">
    <Dialog />
  </Modal>
</ModalOverlay>

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@lixiaoyan lixiaoyan changed the title feat(RAC): expose --page-width and --visual-viewport-width from M… feat(RAC): expose --page-width and --visual-viewport-width from Modal Dec 10, 2025
@lixiaoyan lixiaoyan force-pushed the patch-6 branch 2 times, most recently from 09625c8 to 677c5c5 Compare December 10, 2025 11:18
snowystinger
snowystinger previously approved these changes Dec 10, 2025
Copy link
Copy Markdown
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

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

Seems reasonable

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

@devongovett Hi, any chance to get this merged before next release? This is necessary for our project.

@snowystinger
Copy link
Copy Markdown
Member

@lixiaoyan I'll ask the team, but just to set realistic expectations, it's unlikely to be added this release. We're just a little too late in the cycle with our main priorities on S2 going to 1.0 and our new docs site. I think I can say we would all think this is fairly low risk because it matches the equivalent for height, but it would still need to be discussed and tested. You can use it with a yarn patch or something equivalent in the meantime. Thank you for understanding.

@snowystinger
Copy link
Copy Markdown
Member

I brought it up, the team judged it was just too close to release to take this in.

In addition, we were curious if you had tried using 100vw and if so, what part didn't work for you? An example would be useful to show the use case for this.

@snowystinger snowystinger added the waiting Waiting on Issue Author label Dec 16, 2025
@rothsandro
Copy link
Copy Markdown
Contributor

Using 100vw (and likely also --page-width) for the modal overlay width results in a horizontal scrollbar during the animation, if the page has a vertical scrollbar (that is always visible and not just on scroll). This can be reproduced in the RAC docs:

modal

@snowystinger snowystinger removed the waiting Waiting on Issue Author label Dec 17, 2025
@snowystinger
Copy link
Copy Markdown
Member

Using 100vw (and likely also --page-width) for the modal overlay width results in a horizontal scrollbar during the animation, if the page has a vertical scrollbar (that is always visible and not just on scroll). This can be reproduced in the RAC docs:

I only see the scrollbars the first time I close a Modal, after that it's fine. Given that it has 100vw both times, it doesn't seem like that is the culprit?

@rothsandro
Copy link
Copy Markdown
Contributor

I only see the scrollbars the first time I close a Modal, after that it's fin

Hm, for me it happens always. The new docs page doesn't have a vertical scrollbar but after closing the dialog a vertical scrollbar appears and stays there, and a horizontal scrollbar appears during the animation. The old docs page already had a vertical scrollbar, the issue there was just a temporary horizontal scrollbar during the animation.

Adobe Spectrum does have this problem (and doesn't use 100vw).

modal-scrollbar.mp4

@snowystinger
Copy link
Copy Markdown
Member

snowystinger commented Dec 17, 2025

Odd, now I'm incapable of reproducing in Chrome or Safari or Firefox. Neither Modal or Dialog docs pages in either library
Chrome 143.0.7499.41
Safari 26.1
Firefox 146

@rothsandro
Copy link
Copy Markdown
Contributor

If you are on macOS, did you enable "Show scrollbar: always" in the OS settings? My screenshot is from macOS with this setting enabled, the video is from Windows (which always shows scrollbars, not just on scroll).

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

Anyhow, the --page-width does not have any equivalents.

@snowystinger
Copy link
Copy Markdown
Member

If you are on macOS, did you enable "Show scrollbar: always" in the OS settings? My screenshot is from macOS with this setting enabled, the video is from Windows (which always shows scrollbars, not just on scroll).

Sorry, I should have specified, I always have scrollbars on. I dislike hiding them :)

Anyhow, the --page-width does not have any equivalents.

But what do you need it for? Can you provide an example which demonstrates what you'd use it for? I know there's a bit of an example in the description, but I unfortunately may make different assumptions about setup.

@rothsandro
Copy link
Copy Markdown
Contributor

Oh, just found out it's caused by the 1Password browser extension, so nothing you have to worry about. I should have tested this in a clean browser profile 😕 (And my comment above is wrong, I meant it does not happen in Adobe Spectrum, that's why assume it's somehow related to the 100vw). Sorry for the interruption here.

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

lixiaoyan commented Dec 18, 2025

@snowystinger

The backdrop (ModalOverlay) should cover everything below. We need --page-width to specify its width.

image

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

In addition, we were curious if you had tried using 100vw and if so, what part didn't work for you? An example would be useful to show the use case for this.

In our project, we use this to implement the drawer component:

<ModalOverlay className="absolute top-0 left-0 h-(--page-height) w-(--page-width)">
  <Modal className="grid justify-items-end sticky top-0 left-0 h-(--visual-viewport-height) w-(--visual-viewport-width)">
    <Dialog />
  </Modal>
</ModalOverlay>

If we use 100vw instead, the drawer will exceed the viewport and have its right side clipped.

image

@snowystinger
Copy link
Copy Markdown
Member

I'm really sorry, but I'm not understanding those pictures. Would you mind taking our Tailwind Starter https://react-aria.adobe.com/getting-started#storybook-starter-kits and modifying that to show the problem then pushing it to Github and sharing it here? That way we can run it and it'll be as close to your setup as possible.

Otherwise, I need some really thorough instructions on how to get to those images.

Some things to keep in mind. 100vw doesn't take into account scrollbars, but 100% will, so as long as you have width: 100% on all the parents leading up to the html document, it shouldn't be affected in that way.

It's worth noting that the reason for page-height and viewport-height variables was specifically due to iOS nav bars, which only affect height.

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

lixiaoyan commented Dec 19, 2025

@snowystinger https://github.com/lixiaoyan/rac-modal-horizontal-scroll

Screen.Recording.2025-12-19.at.9.46.37.PM.mov
100% 100vw --page-width and --visual-viewport-width
Document is scrolled to the start Screenshot 2025-12-19 at 3 23 21 PM Screenshot 2025-12-19 at 3 23 26 PM Screenshot 2025-12-19 at 3 23 30 PM
Document is scrolled at the center Screenshot 2025-12-19 at 3 23 36 PM Screenshot 2025-12-19 at 3 23 39 PM Screenshot 2025-12-19 at 3 23 41 PM

However, there is one remaining issue with the new approach offered in this PR: the scrollbar gutter clips the drawer's right side. It's related to a CSS spec issue: w3c/csswg-drafts#8099. Chrome includes the scrollbar gutter's width in the visual viewport width, while Firefox and Safari do not. #9199 has the same root cause.

As I suggested in #9199 (comment), we may need a new util to calculate the visual viewport size without the scorllbar.

const width = Math.min(visualViewport.width, document.documentElement.clientWidth);
const height = visualViewport.height;

Edited: document.documentElement.clientWidth does not work either for stable scrollbar gutters. ref

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

lixiaoyan commented Dec 19, 2025

Some things to keep in mind. 100vw doesn't take into account scrollbars, but 100% will, so as long as you have width: 100% on all the parents leading up to the html document, it shouldn't be affected in that way.

The <ModalOverlay> is an absolute-positioned element rather than a fixed one. When a document is horizontally scrolled, it might be outside the viewport. An element cannot be absolute in the y-axis and fixed in the x-axis simultaneously.

To quickly reproduce, goto https://react-spectrum.adobe.com/Dialog and set the width: 200vw on the <body>. Scroll the document horizontally, then click the "Open Dialog" button.

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

lixiaoyan commented Dec 19, 2025

After some investigations, I discovered that, regardless of iOS, even when using position: fixed to place the ModalOverlay, some tricks are still necessary. Prior to Chrome 136, with scrollbar-gutter: stable; on <html>, an element with position: fixed; inset: 0; would extend to the scrollbar gutter area, resulting in anything on the right side being clipped. It was just fixed earlier this year; it's worth having a workaround.
https://bugs.chromium.org/p/chromium/issues/detail?id=1251856

image

^ Screenshot of https://jsbin.com/vefumujapa/1/edit?html,css,js,output with an older version Chrome

Comment thread packages/@react-aria/utils/src/useViewportSize.ts

let visualViewport = typeof document !== 'undefined' && window.visualViewport;

export function useViewportSize(): ViewportSize {
Copy link
Copy Markdown
Contributor Author

@lixiaoyan lixiaoyan Dec 19, 2025

Choose a reason for hiding this comment

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

We might rename this useVisualViewportSize because it returns the visual viewport size (does not include the scrollbar) but not the 100vw one.

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

@devongovett WDYT?

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

Tested with https://interop-2022-viewport.netlify.app/

is the scrollbar width "excluded"? scrollbar is present stable scrollbar gutter
(scrollbar is not present)
100vw/100dvw
position: absolute and width: 100%
position: fixed and width: 100% Chrome: ⚠️, Others: ✅
visualViewport.width Chrome: ❌, Others: ✅
documentElement.clientWidth Chrome: ❌, Others: ✅
documentElement.getBoundingClientRect().width
ICB (initial containing block) width

// The visual viewport width may include the scrollbar gutter. We should use the minimum width between
// the visual viewport and the document element to ensure that the scrollbar width is always excluded.
// See: https://github.com/w3c/csswg-drafts/issues/8099
? Math.min(visualViewport.width * visualViewport.scale, document.documentElement.clientWidth)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, this line still cannot get the correct ICB width. We may need to insert an empty <div> with position: absolute; width: 100% inside <body> and use its clientWidth for the ICB width. document.documentElement.getBoundingClientRect().width may also work, as long as no width style applied to the <html>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

based on your other comment, this was fixed in Chrome 136? Or was that a different issue? Do we still need this?

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

lixiaoyan commented Dec 19, 2025

Another option is to use something like left-(--page-scroll-left) on the <ModalOverlay>, although it looks a little strange.

<ModalOverlay className="absolute top-0 left-(--page-scroll-left) h-(--page-height) w-full">
  <Modal className="grid justify-items-end sticky top-0 h-(--visual-viewport-height) w-full">
    <Dialog />
  </Modal>
</ModalOverlay>

Edited: I filled a new PR with the method described above. #9383

@github-actions github-actions Bot added the RAC label Mar 16, 2026
Copy link
Copy Markdown
Member

@devongovett devongovett left a comment

Choose a reason for hiding this comment

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

I think we're ok with this. Just one question above on the change to useViewportSize.

Copy link
Copy Markdown
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

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

comment can be answered and we can follow it up

@snowystinger snowystinger enabled auto-merge March 17, 2026 04:35
@snowystinger snowystinger added this pull request to the merge queue Mar 17, 2026
Merged via the queue into adobe:main with commit fde2a81 Mar 17, 2026
28 checks passed
@lixiaoyan
Copy link
Copy Markdown
Contributor Author

@devongovett

Unfortunately, the latest version of Chrome still has issues, meaning the changes made to useViewportSize in this PR are ineffective.

The fix introduced in Chrome 136 only addresses the positioning of position: fixed elements. When retrieving dimensions via DOM APIs, the returned values still incorrectly include the scrollbar width.

Consequently, even with this PR in its current state, content will still be obscured by the scrollbar in the latest version of Chrome (v146). Currently, there is no reliable DOM API to accurately get the viewport width excluding the scrollbar in Chrome.

I can think of two viable approaches moving forward:

  1. Read the <body> width: This might yield incorrect values depending on the user's CSS. Floating UI handles this by using a set of heuristics to discard invalid values (e.g., only accepting the value if its difference from the documentElement width is below a specific threshold).
  2. Inject a measuring element: Append an empty <div> to the <body> with width: 100% to measure the viewport width without the scrollbar. (position: absolute is recommended here, as position: fixed has bugs in Chrome versions prior to 136). While this approach is more complex and could still be affected by user CSS, it is generally more robust and less prone to errors than the first option.

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

Calling getBoundingClientRect on documentElement might also work, though it will likely require additional calculations. You can test and compare the differences between various calculation methods here: https://interop-2022-viewport.netlify.app/scrollbar-gutter/short-stable/, or refer to Floating UI's implementation: https://github.com/floating-ui/floating-ui/blob/master/packages/dom/src/utils/getViewportRect.ts.

Additionally, this useViewportSize issue is also the root cause of #9199.

@nwidynski
Copy link
Copy Markdown
Contributor

nwidynski commented Apr 15, 2026

@lixiaoyan A bit hacky, but how about calculating the visual viewport ourselves like this:

let style = getComputedStyle(document.documentElement);
let scrollBarStyle = style.scrollbarWidth;
let scrollWidth = document.documentElement.scrollWidth;
document.documentElement.style.scrollbarWidth = 'none';
let scrollBarWidth = document.documentElement.scrollWidth - scrollWidth;
document.documentElement.style.scrollbarWidth = scrollBarStyle;
let visualViewportWidth = window.innerWidth - scrollBarWidth;

Seems to work in all browsers pretty consistently and avoids this large mess of a polyfill. To make this even more reliable, while also avoiding potential flicker, we could also copy style from the documentElement onto a hidden element, then measure the scrollbar there - similar to our getOffsetType utils.

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

@nwidynski
Copy link
Copy Markdown
Contributor

nwidynski commented Apr 21, 2026

@lixiaoyan Yes, I understand what the issue is about. Thanks for filing it btw.

Without this behavior change, there is no reliable way to get the viewport width excluding the scrollbar gutter in Chrome.

What I don't understand is why we can't use window.innerWidth - scrollbarGutterWidth as a polyfill for document.documentElement.clientWidth in the meantime.

@lixiaoyan
Copy link
Copy Markdown
Contributor Author

lixiaoyan commented Apr 21, 2026

@nwidynski We can, but we still have no reliable way to determine the scrollbar gutter width either. From the library's perspective, the user may override the scrollbar style on the <html> element. We've already included something similar in our app code, but that approach isn't suitable for React Aria.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants