{"componentChunkName":"component---src-templates-post-jsx","path":"/blog/the-root-scroller-and-how-not-to-lose-it/","result":{"data":{"mdx":{"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nconst _frontmatter = {\n  \"title\": \"The root scroller and how not to lose it\",\n  \"cover\": \"/blogs/rootscroller/cover.svg\",\n  \"date\": \"2026-09-23\",\n  \"type\": [\"tutorial\"]\n};\nconst layoutProps = {\n  _frontmatter\n};\nconst MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  let {\n    components\n  } = _ref,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, `Every page has one main scroll container that scrolls the page itself: the root scroll container, or root scroller for short. Because it scrolls the entire page, the browser gives the root scroller a bunch of features that regular scroll containers don't get, and because they work automatically, most developers aren't aware of them.`), mdx(\"p\", null, `It's easy to accidentally stop using the root scroller, and hard to notice when you have. Your 'page' still scrolls (visually at least) and you don't get any errors, but your site misses out on some features and that can make the user experience worse.`), mdx(\"p\", null, `In this article we'll go over what the root scroller is, how it's different from other scroll containers, and how to fix it when you've accidentally opted out of it.`), mdx(\"h2\", {\n    \"id\": \"what-are-scroll-containers\"\n  }, `What are scroll containers?`), mdx(\"p\", null, `A scroll container is any element that lets you scroll through content that doesn't fit inside it (which sometimes is referred to as overflowing content).`), mdx(\"p\", null, `An element becomes a scroll container when you give it an `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://developer.mozilla.org/en-US/docs/Web/CSS/overflow\"\n  }), mdx(\"code\", _extends({\n    parentName: \"a\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow`)), ` value of `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `auto`), `, `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `scroll`), ` or `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `hidden`), ` and fixed (explicit height or width) or constrained (like max-width, grid areas or flex constraints) dimensions. Overflow set to `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `auto`), ` and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `scroll`), ` means users can scroll it with the mouse (and keyboard, when focused). With `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `hidden`), ` they can't, but it's still a scroll container, and you can still scroll it programmatically.`), mdx(\"p\", null, `When a scroll container sits inside the page or inside another scroll container, it's called a nested scroll container.`), mdx(\"p\", null, `Try the different overflow values below to see how they affect the scroll container behavior.`), mdx(ScrollContainerBasicExample, {\n    mdxType: \"ScrollContainerBasicExample\"\n  }), mdx(\"h2\", {\n    \"id\": \"what-is-the-root-scroller\"\n  }, `What is the root scroller?`), mdx(\"p\", null, `When your content is taller than the viewport, the entire viewport becomes a scroll container. The specs call this the viewport's scrolling box, but browser engineers refer to it as the root scroller, and that's the name we'll use here.`), mdx(\"p\", null, `The browser knows what the root scroller is, and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `document.scrollingElement`), ` returns the element that represents it:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"js\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-js\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), `document`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), `scrollingElement`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token comment\"\n  }), `// <html> in standards mode`)))), mdx(\"p\", null, `In standards mode this is the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<html>`), ` element and in quirks mode it's `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), `. That never changes for a given document, so `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `document.scrollingElement`), ` is mostly just a convenient way to access the root scroller without having to check both elements or parsing the doctype.`), mdx(\"h3\", {\n    \"id\": \"viewports-and-the-root-scroller\"\n  }, `Viewports and the root scroller`), mdx(\"p\", null, `When dealing with the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `html`), ` and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `body`), ` elements, the browser sometimes applies styles and overflow behavior differently than it would for other elements: styling sometimes gets propagated to the viewport rather than being set on the element itself. This propagation was introduced to ensure that the viewport reflects the intended styles even when the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `html`), ` and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `body`), ` elements do not fill the entire viewport.`), mdx(\"p\", null, `One that you might have encountered before is that adding a background to `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `body`), ` will add it to the entire viewport even if body doesn't fill the full height. The background is \"pushed back\" to the root scroller.`), mdx(\"p\", null, `That is, unless you also have a background set on the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `html`), ` element, in which case `, mdx(\"em\", {\n    parentName: \"p\"\n  }, `that`), ` background is applied to the viewport, and the one you set on the body behaves like a normal background. Try it out for yourself below:`), mdx(BackgroundPropagationExample, {\n    mdxType: \"BackgroundPropagationExample\"\n  }), mdx(\"p\", null, `The same happens to other properties that affect layout and scrolling. That propagation behavior is not something you commonly need to care about, but being aware of it can save you a few head scratchers.`), mdx(\"p\", null, `The `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://developer.mozilla.org/en-US/docs/Web/CSS/overflow\"\n  }), mdx(\"code\", _extends({\n    parentName: \"a\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow`)), ` CSS property controls how overflow works, and so creates scroll containers. The value set on `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<html>`), ` doesn't apply to the element itself but is propagated to the viewport. If `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<html>`), ` has the default value `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow: visible`), `, the overflow value of `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` is propagated instead. We'll need this rule later on.`), mdx(\"h2\", {\n    \"id\": \"what-the-root-scroller-gets-that-nested-scroll-containers-dont\"\n  }, `What the root scroller gets that nested scroll containers don't`), mdx(\"p\", null, `There is nothing wrong with using nested scroll containers. Browsers treat them as independent elements with their own scrolling context, and a user can scroll them without affecting the rest of the page.`), mdx(\"p\", null, `Sometimes though, you accidentally create a nested scroll container that encompasses the entire viewport. When you do that, a mouse wheel or touch scroll will scroll the nested container. When that happens, you lose special behavior given to the root scroller:`), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, `Scroll restoration.`), ` When someone navigates back to your page, browsers restore the scroll position of the root scroller. Support for restoring nested scroll containers is inconsistent, so users often end up back at the top instead of where they were.`), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, `Keyboard scrolling.`), ` Space, Page Down and the arrow keys scroll the root scroller by default. A nested scroll container needs to be clicked or focused into first.`), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, mdx(\"code\", _extends({\n    parentName: \"strong\"\n  }, {\n    \"className\": \"language-text\"\n  }), `window.scrollY`), ` and scroll events on `, mdx(\"code\", _extends({\n    parentName: \"strong\"\n  }, {\n    \"className\": \"language-text\"\n  }), `window`), ` only report the root scroller.`), ` Methods like `, mdx(\"code\", _extends({\n    parentName: \"li\"\n  }, {\n    \"className\": \"language-text\"\n  }), `window.scrollTo()`), `, `, mdx(\"code\", _extends({\n    parentName: \"li\"\n  }, {\n    \"className\": \"language-text\"\n  }), `window.scrollBy()`), ` only apply to the root scroller. So anything that relies on these for getting and setting the scroll position will not work correctly with nested scroll containers.`), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, `Printing and screenshots.`), ` Printing uses the height of the root scroller. A page locked to the viewport height prints as a single page and cuts off the rest. Full-page screenshots have the same problem.`)), mdx(\"h4\", {\n    \"id\": \"on-mobile-specifically\"\n  }, `On mobile specifically`), mdx(\"p\", null, `Though scroll-restoration is annoying everywhere, mobile browsers include more scroll-specific functionality that depends on the root scroller, and it's where things will feel more 'broken' as users interact with your website.`), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, `Mobile browser UI.`), ` On phones the address bar and toolbar collapse when the root scroller scrolls. With a nested scroll container they stay visible, so your users have less screen space. (Mobile browser UI has a few more quirks, which we covered in `, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://polypane.app/blog/using-safe-area-inset-to-build-mobile-safe-layouts/\"\n  }), `using safe-area-inset to build mobile safe layouts`), `.)`), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, `Tap to scroll to top.`), ` On iOS, tapping the status bar scrolls the root scroller to the top. It does nothing for nested scroll containers.`), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, `Pull to refresh and overscroll.`), ` These also only work on the root scroller.`)), mdx(\"p\", null, `The example below simulates these behaviors, toggle between the root and nested scrollers and see the difference when doing those actions above: scrolling to move the browser UI out of the way, clicking the top bar to scroll back to the top, and pulling down to refresh the page. When scrolled, reloading the page with the reload button will also restore the scroll position for the root scroller, but not for the nested scroller.`), mdx(MobileRootScrollerExample, {\n    mdxType: \"MobileRootScrollerExample\"\n  }), mdx(\"h2\", {\n    \"id\": \"how-you-accidentally-opt-out\"\n  }, `How you accidentally opt out`), mdx(\"p\", null, `The most common version is adding an app shell or 'wrapper' like this:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `html,\nbody`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100%`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` hidden`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), `\n\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `.page-wrapper`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100vh`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` auto`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, `When you look at your page it still looks the same, but you can see that the dashed border i added around body is fully visible at the bottom, while you would expect it to be pushed down by the content.`), mdx(PageWrapperExample, {\n    mdxType: \"PageWrapperExample\"\n  }), mdx(\"p\", null, `The content still scrolls, but it's `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `.page-wrapper`), ` that is scrolling instead of the viewport. This is usually done on purpose, for a fixed header and sidebar with a scrolling content area, a sticky footer layout, or to prevent the page from scrolling behind a modal. Sometimes a framework sets it up for you.`), mdx(\"p\", null, `The result is a root scroller that doesn't scroll, since that responsibility has been taken over by the nested scroll container.`), mdx(\"h3\", {\n    \"id\": \"accidentally-opting-out-with-overflow-propagation\"\n  }, `Accidentally opting out with overflow propagation`), mdx(\"p\", null, `Another common way to accidentally opt out of the root scroller is by setting that `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow-x`), ` property on both the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `html`), ` and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `body`), ` elements to prevent horizontal overflows, along with `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `height: 100%`), ` to make the page at least as tall as the viewport and keep footers are where you want them. Variants of this can be found in many CSS boilerplates and frameworks.`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `html,\nbody`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100%`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow-x`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` hidden`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, `Because of the propagation rule, the overflow of `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<html>`), ` goes to the viewport. Since `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<html>`), ` is no longer set to `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow:visible`), `, the overflow of `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` stays on `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), `. And when one axis is `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `hidden`), `, the other axis computes to `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `auto`), `. So `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` becomes a scroll container.`), mdx(\"p\", null, `Now just the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow-x: hidden`), ` declaration on `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` is not a problem. `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` still grows with its content because of its natural block layout and doesn't become a scroll container.`), mdx(\"p\", null, `That is, until you give it a fixed height with `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `height: 100%`), `. Now `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` is only as tall as the viewport and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` scrolls instead of the root scroller. Both declarations are common on their own, so it's easy to end up with the combination without realising it.`), mdx(\"p\", null, `Try the example below to see how the overflow is both propagated to the viewport, and causes the default values to change. See if you can guess when you've opted out of the root scroller, then scroll to see if you were right.`), mdx(OverflowPropagationExample, {\n    mdxType: \"OverflowPropagationExample\"\n  }), mdx(\"h2\", {\n    \"id\": \"how-to-detect-when-youve-opted-out-of-the-root-scroller\"\n  }, `How to detect when you've opted out of the root scroller`), mdx(\"p\", null, `Scroll down your page a bit and run this in the console:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"js\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-js\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), `window`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), `scrollY`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`)))), mdx(\"p\", null, `If this returns 0 while you've clearly scrolled, you've opted out of the root scroller and something else is scrolling. Here's that check side by side, for a root scroller and for a nested wrapper:`), mdx(ScrollYSideBySideExample, {\n    mdxType: \"ScrollYSideBySideExample\"\n  }), mdx(\"p\", null, `To find out which element is scrolling, listen for scroll events in the capture phase. Scroll events on elements don't bubble, but a capturing listener on the document does catch all of them.`), mdx(\"p\", null, `Paste this into your console, then scroll:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"js\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-js\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), `document`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token function\"\n  }), `addEventListener`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token string\"\n  }), `'scroll'`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `,`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token parameter\"\n  }), `e`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token operator\"\n  }), `=>`), ` console`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token function\"\n  }), `log`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), `e`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), `target`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `,`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token literal-property property\"\n  }), `capture`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token operator\"\n  }), `:`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token boolean\"\n  }), `true`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`)))), mdx(\"p\", null, `If it logs `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `#document`), `, the root scroller is working. If it logs an element like `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `div.page-wrapper`), `, that's your nested scroll container.`), mdx(\"p\", null, `The `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"/docs/elements-panel/\"\n  }), `Elements panel`), ` in Polypane shows a \"scroll\" badge next to elements that are scroll containers, so you can spot them while going through the DOM tree.`), mdx(\"video\", {\n    src: scrollhighlight,\n    muted: true,\n    controls: true,\n    preload: \"auto\",\n    loading: \"lazy\",\n    playsInline: true,\n    className: \"imgshadow\",\n    style: {\n      maxWidth: '100%'\n    }\n  }), mdx(\"p\", null, `Starting in Polypane 31, elements that are currently being scrolled will have their scroll badge highlighted so you know exactly which element is handling the scroll.`), mdx(\"p\", null, `To list all vertical scroll containers on a page at once, run this in the console:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"js\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-js\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `[`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token operator\"\n  }), `...`), `document`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token function\"\n  }), `querySelectorAll`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token string\"\n  }), `'*'`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `]`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token function\"\n  }), `filter`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token parameter\"\n  }), `el`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token operator\"\n  }), `=>`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token keyword\"\n  }), `const`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), ` overflowY `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token operator\"\n  }), `=`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token function\"\n  }), `getComputedStyle`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), `el`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token keyword\"\n  }), `return`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `[`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token string\"\n  }), `'auto'`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `,`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token string\"\n  }), `'scroll'`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `,`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token string\"\n  }), `'hidden'`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `]`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token function\"\n  }), `includes`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `(`), `overflowY`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token operator\"\n  }), `&&`), ` el`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), `scrollHeight `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token operator\"\n  }), `>`), ` el`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `.`), `clientHeight`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `)`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`)))), mdx(\"p\", null, `Once you know what you're looking for, seeing it on a real phone is easy too. If the address bar doesn't collapse when you scroll, or tapping the status bar doesn't scroll you back up, the root scroller isn't being used.`), mdx(BlogMidCTA, {\n    mdxType: \"BlogMidCTA\"\n  }), mdx(\"h3\", {\n    \"id\": \"testing-it-in-polypane\"\n  }, `Testing it in Polypane`), mdx(\"p\", null, `Most of these issues only show up on touch devices, with a keyboard or when printing, so you won't see them with a mouse on a desktop viewport. But they'll still feel broken to users interacting with your site in those ways.`), mdx(\"p\", null, `Test your pages at mobile sizes with touch events enabled using `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://polypane.app/docs/emulation/\"\n  }), `device emulation`), `, try scrolling with only your keyboard, and run the capture listener above.`), mdx(\"h3\", {\n    \"id\": \"another-way-it-surfaces-in-polypane\"\n  }, `Another way it surfaces in Polypane`), mdx(\"p\", null, `If you use Polypane, `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"/docs/synced-interactions/\"\n  }), `scroll syncing`), ` and `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"/docs/making-screenshots/\"\n  }), `full height screenshots`), ` can give it away too.`), mdx(\"p\", null, `Polypane syncs the scroll position of all scroll containers, root and nested, across panes.`), mdx(\"p\", null, `But if you accidentally create a nested scroll container at a specific breakpoint, for example by setting `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow`), ` inside a media query, the page is scrolled by the root scroller in some panes and by the newly created nested scroll container in others. Those aren't the same scroll container, so their scroll positions can't be synced.`), mdx(\"p\", null, `If scrolling stays in sync between your desktop panes but your mobile pane doesn't follow along (or the other way around), check which element is scrolling in that pane using the methods described above.`), mdx(\"p\", null, `For full height screenshots, Polypane takes the height of the root scroller to determine the total height of the page. If that's just a single viewport, then the resulting full height screenshot will be too.`), mdx(\"h2\", {\n    \"id\": \"keeping-the-root-scroller-in-common-layouts\"\n  }, `Keeping the root scroller in common layouts`), mdx(\"p\", null, `You usually end up with a nested scroll container while building a specific layout. Each of these layouts can be built while keeping the root scroller, so let's go through the common ones.`), mdx(\"h3\", {\n    \"id\": \"make-the-page-fill-at-least-the-full-screen\"\n  }, `Make the page fill at least the full screen`), mdx(\"p\", null, `For example so the footer sits at the bottom of the screen on short pages. A common way to do that is to make a wrapper exactly as tall as the viewport and let the content scroll inside it:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `html,\nbody`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100%`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` hidden`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), `\n\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `.page-wrapper`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `display`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` flex`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `flex-direction`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` column`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100vh`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), `\n\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `main`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `flex`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 1`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` auto`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, `What you actually want is a page that is `, mdx(\"em\", {\n    parentName: \"p\"\n  }, `at least`), ` as tall as the screen, so use `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `min-height`), ` instead of `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `height`), ` and let the document grow with its content:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `.page-wrapper`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `display`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` flex`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `flex-direction`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` column`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `min-height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100svh`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), `\n\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `main`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `flex`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 1`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, `You also don't really need the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow: hidden`), ` on `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `html`), ` and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `body`), ` either. Using `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://developer.mozilla.org/en-US/docs/Web/CSS/length#relative_length_units_based_on_viewport\"\n  }), mdx(\"code\", _extends({\n    parentName: \"a\"\n  }, {\n    \"className\": \"language-text\"\n  }), `svh`), ` or `, mdx(\"code\", _extends({\n    parentName: \"a\"\n  }, {\n    \"className\": \"language-text\"\n  }), `dvh`)), ` instead of `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `vh`), ` also avoids the mobile issue where `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `100vh`), ` is taller than the visible area. We wrote more about the small viewport units when we added them to `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://polypane.app/blog/polypane-26-accurate-device-emulation-with-safe-area-and-small-viewport-units/\"\n  }), `Polypane's device emulation`), `.`), mdx(\"p\", null, `We can go a bit further too, since you probably don't even need a dedicated wrapper element for the page at all. Remove the wrapper entirely and apply the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `min-height`), ` and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `flex`), ` properties directly to the `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `body`), ` and `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `main`), ` elements:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `body`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `min-height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100svh`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `display`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` flex`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `flex-direction`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` column`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), `\n\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `main`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `flex`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 1`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, `The `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `body`), ` can be a wrapper just as much as a dedicated `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `.page-wrapper`), ` element, simplifying your layout and keeping the root scroller intact by not restricting the height of the body element.`), mdx(\"h3\", {\n    \"id\": \"have-a-header-and-sidebar-that-stay-in-place\"\n  }, `Have a header and sidebar that stay in place`), mdx(\"p\", null, `This is the app shell layout: a header at the top, a sidebar on the side and only the content area scrolling. It's often built as a grid that's exactly as tall as the viewport, with the content area as a scroll container. This fits a nice mental model of the viewport as a fixed frame. The web isn't a fixed frame though. The viewport is just that, a view into a part of the document.`), mdx(\"p\", null, `You can keep the root scroller intact by using `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `position: sticky`), ` and using a min-height on the sidebar to get the same layout. The page scrolls normally and the header and sidebar stick to the viewport:`), mdx(HeaderSidebarStickyExample, {\n    mdxType: \"HeaderSidebarStickyExample\"\n  }), mdx(\"p\", null, `The sidebar can still scroll on its own if it's long (becoming its own scroll container). That's fine because it's a secondary scroll container, not the page itself. Content inside the main area will scroll with the page as expected.`), mdx(\"p\", null, `If your sticky elements don't stick, check `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://polypane.app/blog/getting-stuck-all-the-ways-position-sticky-can-fail/\"\n  }), `all the ways position: sticky can fail`), `. For grid layouts, it's very easy to create that unexpected scroll container (making sticky elements not stick as intended) or to have the element not able to get stuck, because it stretches the full grid area.`), mdx(\"h3\", {\n    \"id\": \"get-rid-of-a-horizontal-scrollbar\"\n  }, `Get rid of a horizontal scrollbar`), mdx(\"p\", null, `Something on the page is wider than the viewport and causes a horizontal scrollbar, so you hide it:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `html,\nbody`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100%`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow-x`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` hidden`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, `As we saw earlier, when `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `<body>`), ` also has a fixed height, this turns it into the element that scrolls the page because setting `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow: hidden`), ` in one direction automatically changes it from `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `visible`), ` to `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `auto`), ` in the other direction.`), mdx(\"p\", null, `A quick solution is to use `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `clip`), ` instead:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `html,\nbody`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `height`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` 100%`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow-x`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` clip`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `clip`), ` cuts off overflowing content like `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `hidden`), ` does, but it doesn't create a scroll container. The `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `overflow`), ` of the other direction is still set to `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `visible`), `, and its contents will just overflow outside the body's height.`), mdx(HorizontalOverflowExample, {\n    mdxType: \"HorizontalOverflowExample\"\n  }), mdx(\"h4\", {\n    \"id\": \"quick-fix-vs-proper-fix\"\n  }, `Quick fix vs. proper fix`), mdx(\"p\", null, `This is a `, mdx(\"em\", {\n    parentName: \"p\"\n  }, `quick`), ` fix, but it isn't a `, mdx(\"em\", {\n    parentName: \"p\"\n  }, `good`), ` fix, since it hides the symptom (the bar is still overflowing).`), mdx(\"p\", null, `Finding and fixing the element that causes the overflow is the better solution, and our article on `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://polypane.app/blog/strategies-for-dealing-with-horizontal-overflows/\"\n  }), `strategies for dealing with horizontal overflows`), ` shows you how.`), mdx(\"p\", null, `Polypane also `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://polypane.app/docs/horizontal-overflow/\"\n  }), `detects horizontal overflow`), ` for you and highlights the element that causes it.`), mdx(\"h3\", {\n    \"id\": \"stop-the-page-scrolling-behind-a-modal\"\n  }, `Stop the page scrolling behind a modal`), mdx(\"p\", null, `When a modal is open, scrolling inside it shouldn't scroll the page behind it. Some sites solve this by keeping the page in a wrapper with its own scroll container permanently, so they can lock the wrapper while the modal is open.`), mdx(\"p\", null, `You only need to lock the page while the modal is open. With `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://polypane.app/blog/decoding-css-selectors-has-not-vs-not-has/\"\n  }), mdx(\"code\", _extends({\n    parentName: \"a\"\n  }, {\n    \"className\": \"language-text\"\n  }), `:has()`)), ` you can do that in CSS:`), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"css\"\n  }, mdx(\"pre\", _extends({\n    parentName: \"div\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-css\"\n  }), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `html`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `scrollbar-gutter`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` stable`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token selector\"\n  }), `&:has(dialog[open])`), ` `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `{`), `\n    `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token property\"\n  }), `overflow`), mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `:`), ` hidden`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `;`), `\n  `, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`), `\n`, mdx(\"span\", _extends({\n    parentName: \"code\"\n  }, {\n    \"className\": \"token punctuation\"\n  }), `}`)))), mdx(\"p\", null, `Hiding the scrollbar can make the page shift sideways, so we add `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter\"\n  }), mdx(\"code\", _extends({\n    parentName: \"a\"\n  }, {\n    \"className\": \"language-text\"\n  }), `scrollbar-gutter: stable`)), ` to `, mdx(\"code\", _extends({\n    parentName: \"p\"\n  }, {\n    \"className\": \"language-text\"\n  }), `html`), ` to keep the space reserved.`), mdx(\"p\", null, `Toggle the lock off and try scrolling behind an open dialog — then turn it back on:`), mdx(ModalLockExample, {\n    mdxType: \"ModalLockExample\"\n  }), mdx(\"h3\", {\n    \"id\": \"nested-scroll-containers-are-fine-for-specific-use-cases\"\n  }, `Nested scroll containers are fine for specific use cases`), mdx(\"p\", null, `Though most of this article is spent explaining how to avoid creating unwanted scroll containers, there's plenty of situations where you'll want to use them (like the scrollable sidebar in the example above, or a chat window).`), mdx(\"p\", null, `What you want to avoid is accidentally creating a scroll container that covers your whole viewport, but there are plenty of other places where nested scroll containers are perfectly fine.`), mdx(\"p\", null, `If you want to understand how scroll containers fit in with the rest of CSS layout, read `, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://polypane.app/blog/understanding-the-fundamentals-of-css-layout/\"\n  }), `understanding the fundamentals of CSS layout`), `.`), mdx(\"h2\", {\n    \"id\": \"use-the-root-scroller-for-the-main-page\"\n  }, `Use the root scroller for the main page`), mdx(\"p\", null, `I hope this article has helped you understand why using the root scroller for the main scrolling area of your page is important and beneficial for your visitors, and helped you get a better grasp of managing scroll behavior in your own projects.`), mdx(\"p\", null, `Check out some of the tools in Polypane to help you check and manage scroll behavior effectively:`), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, `The code snippets above executed in our `, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"/docs/console/\"\n  }), `console panel`), ` make it really easy to catch differences introduced by media breakpoints, and help you make more resilient websites.`), mdx(\"li\", {\n    parentName: \"ul\"\n  }, `With the `, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"/docs/device-emulation/\"\n  }), `device emulation`), ` in Polypane, you can see how your page behaves across different screen sizes and orientations, ensuring that the root scroller remains consistent and effective.`), mdx(\"li\", {\n    parentName: \"ul\"\n  }, `The `, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"/docs/elements-panel/\"\n  }), `elements panel`), ` in Polypane allows you to inspect and manipulate the DOM, making it easier to understand how scroll containers are applied and how they affect your layout.`)), mdx(\"p\", null, `By combining these tools, you can confidently manage scroll behavior and create more robust and user-friendly web experiences.`));\n}\n;\nMDXContent.isMDXComponent = true;","timeToRead":10,"tableOfContents":{"items":[{"url":"#what-are-scroll-containers","title":"What are scroll containers?"},{"url":"#what-is-the-root-scroller","title":"What is the root scroller?","items":[{"url":"#viewports-and-the-root-scroller","title":"Viewports and the root scroller"}]},{"url":"#what-the-root-scroller-gets-that-nested-scroll-containers-dont","title":"What the root scroller gets that nested scroll containers don't","items":[{"items":[{"url":"#on-mobile-specifically","title":"On mobile specifically"}]}]},{"url":"#how-you-accidentally-opt-out","title":"How you accidentally opt out","items":[{"url":"#accidentally-opting-out-with-overflow-propagation","title":"Accidentally opting out with overflow propagation"}]},{"url":"#how-to-detect-when-youve-opted-out-of-the-root-scroller","title":"How to detect when you've opted out of the root scroller","items":[{"url":"#testing-it-in-polypane","title":"Testing it in Polypane"},{"url":"#another-way-it-surfaces-in-polypane","title":"Another way it surfaces in Polypane"}]},{"url":"#keeping-the-root-scroller-in-common-layouts","title":"Keeping the root scroller in common layouts","items":[{"url":"#make-the-page-fill-at-least-the-full-screen","title":"Make the page fill at least the full screen"},{"url":"#have-a-header-and-sidebar-that-stay-in-place","title":"Have a header and sidebar that stay in place"},{"url":"#get-rid-of-a-horizontal-scrollbar","title":"Get rid of a horizontal scrollbar","items":[{"url":"#quick-fix-vs-proper-fix","title":"Quick fix vs. proper fix"}]},{"url":"#stop-the-page-scrolling-behind-a-modal","title":"Stop the page scrolling behind a modal"},{"url":"#nested-scroll-containers-are-fine-for-specific-use-cases","title":"Nested scroll containers are fine for specific use cases"}]},{"url":"#use-the-root-scroller-for-the-main-page","title":"Use the root scroller for the main page"}]},"excerpt":"Every page has one main scroll container that scrolls the page itself: the root scroll container, or root scroller for short. Because it scrolls the entire page…","wordCount":{"words":2768},"frontmatter":{"title":"The root scroller and how not to lose it","type":["tutorial"],"cover":"/blogs/rootscroller/cover.svg","date":"2026-09-23","updated":null},"fields":{"slug":"/blog/the-root-scroller-and-how-not-to-lose-it/","date":"2026-09-22T22:00:00.000Z","ogFileName":"the-root-scroller-and-how-not-to-lose-it"}}},"pageContext":{"slug":"/blog/the-root-scroller-and-how-not-to-lose-it/","relatedPosts":[{"node":{"fields":{"slug":"/blog/the-intl-api-the-best-browser-api-youre-not-using/","date":"2026-04-07T22:00:00.000Z","updated":"2026-04-07T22:00:00.000Z"},"excerpt":"Chances are you've used  Moment.js ,  date-fns ,  Luxon , or  numeral.js  at some point. Developers have relied on these libraries for years to format dates, numbers, and currencies. Those are all very useful libraries…","timeToRead":13,"frontmatter":{"title":"The Intl API: The best browser API you're not using","cover":"/blogs/intl/cover.png"}}},{"node":{"fields":{"slug":"/blog/not-all-browser-apis-are-web-apis/","date":"2026-01-08T23:00:00.000Z","updated":"2026-01-11T23:00:00.000Z"},"excerpt":"When we talk about \"the web platform\", we often treat it as a unified, standards-based system: browsers implement features from the same specifications, even if they do so in different time frames. That should mean that…","timeToRead":11,"frontmatter":{"title":"Not All Browser APIs Are \"Web\" APIs","cover":"/blogs/browserweb/cover.png"}}},{"node":{"fields":{"slug":"/blog/decoding-css-selectors-has-not-vs-not-has/","date":"2025-02-17T23:00:00.000Z","updated":"2025-02-17T23:00:00.000Z"},"excerpt":"CSS functions like :is(), :not() and :has() are powerful tools that make it much easier to select elements specifically. We've written before about how  ::where() :is() and :has() make your life easier , as well as how…","timeToRead":3,"frontmatter":{"title":"Decoding CSS Selectors: :has(:not) vs :not(:has)","cover":"/blogs/decoding1/cover.svg"}}}]}},"staticQueryHashes":["4164364741","425175329"]}