From fde2a81440225da687b3f906ed792e1021e5a7b1 Mon Sep 17 00:00:00 2001 From: XiaoYan Li Date: Tue, 17 Mar 2026 13:40:27 +0900 Subject: [PATCH] feat(RAC): expose `--page-width` and `--visual-viewport-width` from Modal (#9318) Co-authored-by: Devon Govett --- packages/@react-aria/utils/src/useViewportSize.ts | 14 ++++++++++++-- packages/react-aria-components/src/Modal.tsx | 7 ++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/utils/src/useViewportSize.ts b/packages/@react-aria/utils/src/useViewportSize.ts index fa27847c032..30d25de132a 100644 --- a/packages/@react-aria/utils/src/useViewportSize.ts +++ b/packages/@react-aria/utils/src/useViewportSize.ts @@ -94,10 +94,20 @@ export function useViewportSize(): ViewportSize { return size; } +/** + * Get the viewport size without the scrollbar. + */ function getViewportSize(): ViewportSize { return { // Multiply by the visualViewport scale to get the "natural" size, unaffected by pinch zooming. - width: visualViewport ? visualViewport.width * visualViewport.scale : document.documentElement.clientWidth, - height: visualViewport ? visualViewport.height * visualViewport.scale : document.documentElement.clientHeight + width: visualViewport + // 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) + : document.documentElement.clientWidth, + height: visualViewport + ? visualViewport.height * visualViewport.scale + : document.documentElement.clientHeight }; } diff --git a/packages/react-aria-components/src/Modal.tsx b/packages/react-aria-components/src/Modal.tsx index e5954ce7d33..26dfe984e5b 100644 --- a/packages/react-aria-components/src/Modal.tsx +++ b/packages/react-aria-components/src/Modal.tsx @@ -185,17 +185,22 @@ function ModalOverlayInner({UNSTABLE_portalContainer, ...props}: ModalOverlayInn }); let viewport = useViewportSize(); + let pageWidth: number | undefined = undefined; let pageHeight: number | undefined = undefined; if (typeof document !== 'undefined') { let scrollingElement = isScrollable(document.body) ? document.body : document.scrollingElement || document.documentElement; - // Prevent Firefox from adding scrollbars when the page has a fractional height. + // Prevent Firefox from adding scrollbars when the page has a fractional width/height. + let fractionalWidthDifference = scrollingElement.getBoundingClientRect().width % 1; let fractionalHeightDifference = scrollingElement.getBoundingClientRect().height % 1; + pageWidth = scrollingElement.scrollWidth - fractionalWidthDifference; pageHeight = scrollingElement.scrollHeight - fractionalHeightDifference; } let style = { ...renderProps.style, + '--visual-viewport-width': viewport.width + 'px', '--visual-viewport-height': viewport.height + 'px', + '--page-width': pageWidth !== undefined ? pageWidth + 'px' : undefined, '--page-height': pageHeight !== undefined ? pageHeight + 'px' : undefined };