How to fight the <body> scroll

Anton Korzunov
React Camp
Published in
7 min readJul 19, 2018

First of all — WHY one have to fight the scroll?

In short — because sometimes you have to block all the interactions with the page. Page should be completely irresponsible. Thats the rule. Simple rule.

  • You shall not be able to click on it. Not be able to click.
  • Move the focus, or press keys. Not be able to press.
  • Scroll it. Not be able to scroll.

Ok then — WHEN it should be irresponsible?

When you should be focused not on the page, but only on some part of it.

For example Modals — small(or big) boxes, floating in the middle of the screen. The rest of the page could be even dimmed, cos…

the Modal is the only entity you should operate with.

Modal will block everything, this is https://atlaskit.atlassian.com/packages/core/modal-dialog

I am a bit surprised, that MDN Modal Dialog a11n Article highlights the need of focus management and proper aria attributes, but not scroll blocking.

And not text-to-speech blocking via inert or aria-hidden, right now implemented only in reach-ui and smooth-ui

May be it’s not super clear why modal should block page scroll. Scroll seems to be ok, as long you will see it.

While you are understanding what you are scrolling the page. And while it is what you wanted to do.

Thus another great example, to explain why scroll should be blocked, is Fullscreen Modal Dialog, or “FocusedTask”. Something which will consume all your attention.

Once opened it will consume all the screen, and you will not see and be able interact with the main page content any more. But you will able to scroll it.

Need more? Ok — “onboarding” component, which guides you thought the page.

AtlasKit “onboarding” is awesome — https://atlaskit.atlassian.com/packages/core/onboarding

Let’s imagine — you are able to scroll the page. And you will scroll the page. And “spotlights” will be scrolled out. And your customer will be lost in the partially disabled application……

How to block a scroll?

Oh! That’s super easy — style="overflow:hidden" on body.

overflow:hidden will remove the scrollbars (they are hidden), and block the scroll, as long this overflow mode is not scrollable. This is how CSS works.

All done. You might go home.

But… Safari

Yep— it does not work on Mobile Safari.

Surprise! 🤪

  • Safari ignores overflow on body, allowing you to touch-scroll it. And this is something you can touch-n-feel only on the real iPhone, not Chrome emulation.
  • It will work as a shit, especially for forms, as long “touch-scroll” is “drag-n-drop” action for some tags, and you will not be able to scroll thought them.

The fix for the second problem is simple — just change scroll behavior:

> This might be something you want enabled by default!

And the fix for the first problem is also… simple… — event.preventDefault.

The only problem — which element to listen, and which event to prevent. All events inside the modal should go life, all events outside should be blocked, and all those events could bubble from “ok-ish” elements, to the not-ok-ish.

So — for every event, which might cause scroll, you have to calculate something, lets call it scrollability, and block event which might scroll something you don’t want to be scrolled.

I would say — use external libraries to handle this.

Conclusion

So — use overflow:hidden on body to kill the scroll on Desktop and Android browsers, and prevent events to mitigate Safari.

Easy?

Actually not, we missed one point. Scroll bars!

Kill the The Scroll Bar!

Scroll is disabled only when scroll bars are disabled.

Question — you had scroll bar, and now you have removed it. That is left?

Scroll bars disappeared! Completely!

I have no Window machine, sorry

As result you page now 17 pixels wider, and all your content “jumped”. Thats parasite and quite unpleasant jittery effect we shall not stand.

It is easy to mitigate it — detect Scroll bar width, as set as a padding to body.

And last “bonus” stuff — properly handle width of the body: normal elements must have “right” there it was before, ie with “gap”, while “right” for elements inside lock should be on the window right.

Filling “gap” with paddingRight.

Notice — absolute positioned elements stick to window edge. That might be something you want — display element from edge to edge. But, you know, content of those elements will “shake” and probably this is not what you really want.

Filling the “gap” with marginRight.

Notice — first(red), and third(no name) element are not changing their position, while second (position:fixed) still stick to the edge. It’s fixable — as long as we “fixed” body — we might fix any other element.

Probably you need this behavior.

Fun fact — majority of libraries uses paddings. Any ideas why?

Today there is only one library (and sorry — it’s based on React, feel free to fork and adopt it to your stack) which does it right:

Another conclusion

Step to prevent scroll on <body/> element:

  • Add overflow:hidden on body element.
  • Handle touch events, for Safari.
  • Keep the scroll bar gap.

Plus bonus point: make full screen locks work without “gap”.

And, let me be honest, this is not as easy, as 1–2–3. Details, devil is in details. Please use third party libraries — they will help you. A lot! As usual.

Read

Read more about Scrolling and scrolling issues. This is the best article ever.

Read more about scroll/wheel/touch event canceling

And don’t forget about “proper focus management”, I’ve mentioned in the article beginning, but did not properly explain:

Use

DOM (Vanilla JS)

Body-scroll-lock: will cover all 3 steps. Quote compact. Allows one internal scrollable element inside, which may lead to a real production issue one day. Probably, I would not recommend it, but it I am writing this article due to article about this library, so — thank you body-scroll-lock.

Scroll-lock: Will cover all 3 steps, including nested scrollable containers, and configurable enought, to fit every need. This library got just a few ⭐️, but worth a million.

Dom-Locky: handles only even “preventing” step, but doing it right. You were handling scrolls bar by your self, and need only to handle touch events — use this library.

React

React-scroll-locky: full-feature library to cover 3 steps. Support nested locks, nested scrollable components, and absolutely unbreakable.

React-remove-scroll: another version of scroll-locky, twice smaller and react-portals aware (probably this is the best choice for React)

React-scrolllock: full-feature library to cover 3 steps. Doesn’t support nested locks, nested scrollable components, portals, and uses paddings on body. But I have to mention it, as long as I’ve created my ones as a response to this one.

Extra

Scrollbars are broken. That’s a fact. That’s why we have to hide them, while we have to disable them. Not all the browsers allow you to control them. Not all the browsers even have them. Take your fate in your own hands —

--

--