Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions packages/mui-utils/src/getScrollbarSize/getScrollbarSize.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
// A change of the browser zoom change the scrollbar size.
// Credit https://github.com/twbs/bootstrap/blob/488fd8afc535ca3a6ad4dc581f5e89217b6a36ac/js/src/util/scrollbar.js#L14-L18
// Credit https://github.com/twbs/bootstrap/blob/0907244256d923807c3a4e55f4ea606b9558d0ca/js/modal.js#L214-L221
// and https://github.com/twbs/bootstrap/blob/0907244256d923807c3a4e55f4ea606b9558d0ca/less/modals.less#L122-L128
Comment on lines +2 to +3
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The referenced Bootstrap commit (0907244) is very old (from 2013) and predates modern approaches to handling scrollbar measurements. The linked code uses a different technique than what's implemented here. The Bootstrap reference creates a scrollable div to measure scrollbar width but does not use devicePixelRatio or zoom properties. Consider updating the comment to accurately reflect the actual source of this implementation or remove the misleading citation.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's from 2014, and that is the latest commit to the referenced file. I kept the citation because there was one in the original and I started from it when developing my solution. The devicePixelRatio and zoom parts are mine and are the things that are required for this to work correctly with browser zoom (and very unorthodox dpi/scaling combinations)

It felt best to keep the citation to bootstrap, but I can absolutely take full credit if that's what you prefer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also what is "modern approaches to handling scrollbar measurements" supposed to mean? It's more recent than the commit that was referenced before by mui.

While developing my solution I looked at other libraries, like Bootstrap, Chakra, and FluentUI but they all have the same problem. As far as I can tell my solution to this is unique.

After completing it I did find Vuetify but they take another approach that doesn't hide the scrollbar. Instead they apply position: fixed to the html element and some positioning to keep the scrollbar but prevent scrolling. But implementing this would require much more changes to MUI and I generally prefer smaller changes to large ones.

export default function getScrollbarSize(win: Window = window): number {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
const documentWidth = win.document.documentElement.clientWidth;
return win.innerWidth - documentWidth;
const scrollDiv = win.document.createElement('div');

scrollDiv.style.setProperty('position', 'absolute');
scrollDiv.style.setProperty('top', '-9999px');
scrollDiv.style.setProperty('width', '50px');
scrollDiv.style.setProperty('height', '50px');
scrollDiv.style.setProperty('overflow', 'scroll');
// Invert the zoom level to get 100% sized scrollbars for the element
scrollDiv.style.setProperty('zoom', `${1 / win.devicePixelRatio}`);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The CSS zoom property is non-standard and not supported in Firefox. According to MDN, zoom is deprecated and considered non-standard. Using it will cause this functionality to not work properly in Firefox browsers. Consider using CSS transforms with scale() instead, or investigate alternative approaches that work across all major browsers.

Suggested change
scrollDiv.style.setProperty('zoom', `${1 / win.devicePixelRatio}`);
scrollDiv.style.setProperty('transform-origin', '0 0');
scrollDiv.style.setProperty('transform', `scale(${1 / win.devicePixelRatio})`);

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Valid suggestion

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Zoom is not deprecated, and it is supported in Firefox https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/zoom

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is deprecated is the reset value

Comment on lines +5 to +13
Copy link
Member

Choose a reason for hiding this comment

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

Just trying to understand the code here, why is there a need to create this dummy scroll div? I see it's added in Bootstrap, but can't understand why.

Comment on lines +5 to +13
Copy link
Member

Choose a reason for hiding this comment

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

Why to add this zoom on the dummy div?


win.document.body.append(scrollDiv);

const unZoomedScrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
Copy link
Member

Choose a reason for hiding this comment

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

Does this unZoomedScrollbarWidth has value in float?


win.document.body.removeChild(scrollDiv);
Comment on lines +15 to +19
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The code appends a div to document.body without checking if body exists. In edge cases where this function is called during early page initialization or in unusual document contexts, this could throw an error. Consider adding a check to ensure win.document.body is available before attempting to append the element.

Suggested change
win.document.body.append(scrollDiv);
const unZoomedScrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
win.document.body.removeChild(scrollDiv);
const body = win.document.body;
if (!body) {
return 0;
}
body.append(scrollDiv);
const unZoomedScrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
body.removeChild(scrollDiv);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The function getScrollbarSize is called when we need to apply a scroll lock. That happens on user interaction.

Copy link
Member

Choose a reason for hiding this comment

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

What's the point of adding and removing the div immediately?


const zoomedScrollbarWidth = unZoomedScrollbarWidth / win.devicePixelRatio;
Comment on lines +13 to +21
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The new zoom-aware scrollbar calculation logic lacks test coverage. The existing test only covers custom scrollbar widths but doesn't test the devicePixelRatio-based zoom calculations. Consider adding tests that verify the behavior at different devicePixelRatio values (e.g., 1, 1.5, 2) to ensure the calculations work as expected.

Copilot uses AI. Check for mistakes.

return zoomedScrollbarWidth;
Comment on lines +5 to +23
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The calculation divides by devicePixelRatio, which is incorrect. As devicePixelRatio reflects display density (not browser zoom), this will produce wrong results on high-DPI displays. For instance, on a 2x Retina display with no browser zoom, this would incorrectly halve the scrollbar width. The previous implementation using window.innerWidth - documentElement.clientWidth was the correct approach for measuring scrollbar size and already accounts for zoom naturally.

Suggested change
const scrollDiv = win.document.createElement('div');
scrollDiv.style.setProperty('position', 'absolute');
scrollDiv.style.setProperty('top', '-9999px');
scrollDiv.style.setProperty('width', '50px');
scrollDiv.style.setProperty('height', '50px');
scrollDiv.style.setProperty('overflow', 'scroll');
// Invert the zoom level to get 100% sized scrollbars for the element
scrollDiv.style.setProperty('zoom', `${1 / win.devicePixelRatio}`);
win.document.body.append(scrollDiv);
const unZoomedScrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
win.document.body.removeChild(scrollDiv);
const zoomedScrollbarWidth = unZoomedScrollbarWidth / win.devicePixelRatio;
return zoomedScrollbarWidth;
const documentElement = win.document.documentElement;
return win.innerWidth - documentElement.clientWidth;

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

@ZeeshanTamboli can you verify this, this sounds like a blocker.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is wrong, I've posted other comments that refute the claims about devicePixelRatio. While the previous approach does account for zoom, it fails to do so correctly at the zoom levels that are tricky floats that causes problems with rounding.

Comment on lines +5 to +23
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The new implementation creates a DOM element, appends it to the document body, measures it, and then removes it on every call. This is significantly more expensive than the previous approach which only queried existing DOM properties (window.innerWidth and documentElement.clientWidth). If this function is called frequently (for example, during window resize events or when multiple modals open), this could create performance issues. Consider implementing a caching mechanism or using the previous simpler approach.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Performance concern here. It runs everytime getScrollbarSize is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not something that will happen frequently, it should happen once every time there is the need to apply a scroll lock. Because users are free to change zoom or drag a window to another display whenever they like we cannot simply calculate the value once and use it every time.

Consider what we use the value for, we use it to set padding-right on one or more elements, forcing the browser to reflow the layout. I would be very surprised if that didn't have a higher performance cost.

But there is the option to use the same trick that is used in the devicePixelRatio example I have linked previously and monitor for changes to devicePixelRatio using window.matchMedia() and use it to signal if we need to remeasure the scrollbar width. But this would add more complexity for very little gain.

Comment on lines +12 to +23
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Using devicePixelRatio to account for browser zoom is incorrect. The devicePixelRatio property represents the ratio between physical pixels and CSS pixels (related to display density/DPI), not browser zoom level. For example, a Retina display has a devicePixelRatio of 2 or 3 regardless of browser zoom. When a user zooms to 90%, devicePixelRatio remains unchanged on the same display. This approach conflates two different concepts and will produce incorrect results on high-DPI displays or when browser zoom doesn't match the device pixel ratio.

Suggested change
// Invert the zoom level to get 100% sized scrollbars for the element
scrollDiv.style.setProperty('zoom', `${1 / win.devicePixelRatio}`);
win.document.body.append(scrollDiv);
const unZoomedScrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
win.document.body.removeChild(scrollDiv);
const zoomedScrollbarWidth = unZoomedScrollbarWidth / win.devicePixelRatio;
return zoomedScrollbarWidth;
win.document.body.append(scrollDiv);
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
win.document.body.removeChild(scrollDiv);
return scrollbarWidth;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is incorrect, you can try it out yourself in the demo that I linked earlier: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#examples devicePixelRatio changes with browser zoom.

The table I made earlier is from data gathered using the two different displays I have access to:

  1. Desktop monitor 27" 2560x1440
  2. Laptop screen 16" 1920x1200 (which I'm not sure if it qualifies as "High-DPI" but as you can see in my table it has a devicePixelRatio of 1.25 at 100% zoom)

}
Loading