Locking body scroll for modals on iOS

Revisiting old solutions for preventing background scrolling from within modal dialogs & overlays.

I’m a software engineer living and working in East London. I’m currently helping to build a one-stop-shop for the digitisation of alternative assets over at Daphne . Although once strictly front-end, today I work across the whole stack, including dipping my toes into DevOps and writing  Rust & Go.

Stop me if you’ve heard this one before:

  • You have a site with a fixed overlay, such as a modal.
  • You open the modal and scroll it.
  • By the time you hit the end of the modal (or even sooner), the background behind it starts coming along for the ride.

For most browsers, setting overflow: hidden on the body (once the overlay is open) is sufficient. Not so on mobile Safari:

Over the years loads of smart people, such as Ben Frain , have written about various solutions, ranging from CSS -only tricks to JavaScript event interceptors.

Let’s run through a few of them.

The sledgehammer: listening for touch move

Libraries such as Body Scroll Lock selectively block touchmove events using JavaScript. This mostly works, but feels very heavy-handed.

You also need to know up front which (if any) root element you want to retain scrolling on, such as the inner content of the modal itself. If you don’t get it right, you risk the user not being able to scroll the modal, or getting stuck.

Wishful thinking: overscroll-behaviour and touch-action

For a brief moment after iOS 13 was released it seemed like we finally had our answer: -webkit-overflow-scrolling . Ben’s solution sort of works today, but it’s very easy to still get into weird UI states:

After pinching and zooming, you can still scroll the rest of the page. Even worse, by the end of the above video I can’t move at all, rendering the page broken.

Revisiting a classic: position: fixed

The easy, works-everywhere solution is to use position: fixed on the body. Unfortunately, doing so famously resets the scroll position to the top of the page.

This might be fine if your trigger is at the top anyway, since the user will already be there (or close enough). However, if you’re invoking the overlay from further down the page, such as from sticky navigation, the user will lose their spot.

There are a couple of workarounds:

Top positioning

You can apply a margin or negative top value equal to the scroll distance from the top of the page. Then, when you remove position: fixed , you simply scroll the page to that point. It’s pretty seamless.

However things can get a bit weird if the user resizes the page, or changes device orientation. Since you can’t rely on the browser to reposition the scroll, you risk having an offset so large that it actually sends the entire content off-screen unless you dynamically adjust it.

Scrolling to the old position

An easier solution is to just scroll to the position, rather than setting an offset. The content can never be offscreen, since you’re not applying any kind of fake positioning or negative margin to keep it in view; you’re simply saving and restoring the scroll to what you think it ought to be.

It may be wrong after a resize, but should never result in a broken looking page.

Sticking with the classics

I still think it’s hard to beat position: fixed alongside programmatic scrolling:

  • You don’t need to know which elements need to retain scroll. If you want the overlay to be scrollable, it can be using overflow: auto .
  • You don’t need to worry about the contents flying off-screen due to an offset that no longer applies to the current viewport.
  • It works everywhere, including iOS, without any jank .

Here’s a brief example:

Check out the CodePen to get a feel for it:

See the Pen Overlay-scroll position overflow hidden by Jay Freestone ( @jayfreestone ) on CodePen .

The debug view is also available here if you want to try it out on a mobile device.

Let me know if you run into an easier solution!

Simple Solution to Prevent Body Scrolling on iOS

Posted on 30 October 2019 , by Markus Oberlehner , in Development , tagged JavaScript

ads via Carbon

In my last article about building accessible popup overlays with Vue.js we used a simple technique to prevent scrolling in the background. I think that this little trick for preventing scrolling on the <body> element on all devices, including iOS 12 and below (finally, this was fixed in iOS 13 🎉) is worth taking a closer look.

Usually, we can use overflow: hidden on the <body> element to prevent scrolling. But unfortunately, that does not work on older versions of iOS.

In this article, we check out which possibilities we have to prevent scrolling in all browsers, including mobile devices like iPhones and Android-powered smartphones.

The most straightforward way for disabling scrolling on websites is to add overflow: hidden on the <body> tag. Depending on the situation, this might do the job well enough. If you don’t have to support older versions of iOS, you can stop reading here.

Another way of how to deal with this problem is to use the body-scroll-lock package. This is definitely the most bulletproof way how you can do this. But it comes with the downside of being a pretty complicated solution, which adds 1.1 kB to your final bundle.

Next, we take a look at a not very elegant but simple solution to this problem.

The simple solution for preventing scrolling on iOS

The final size this solution adds to our bundle is only 253 bytes, so significantly less than the body-scroll-lock package.

As you can see above, we use position: fixed in combination with storing the scroll position of the user so we can restore the scroll position after the fact.

There are certainly some downsides to this approach. If you change the size of the browser window while the scroll lock is active, for example, the scroll position does not get restored correctly.

Another thing we have to consider is that setting CSS styles on the body triggers painting in the browser. I don’t think this is a big deal in most cases. But if you need to lock and unlock scrolling very frequently (every couple seconds), this might hurt the frame rate of your application.

But the most critical caveat you have to keep in mind is that this approach changes certain styles on the <body> element. If you apply custom styles for overflow , position , top , or width , your styles might break when the scroll lock is enabled.

Furthermore, there might be some edge cases I didn’t think of, and the developers of body-scroll-lock have. But until this point, I got along pretty well using this approach on a couple of sites.

Like What You Read?

Follow me to get my latest articles.

Wrapping it up

It’s unfortunate that for a long time, only using overflow: hidden to prevent scrolling did not work on iOS. But with only 18 lines of JavaScript, we can work around the problem.

In the end, you must decide if it is even necessary in your case to support older versions of iOS. Luckily, iOS users usually do update very quickly.

  • Will Po, Body scroll lock — making it work with everything
  • WebKit Bugzilla,with overflow:hidden CSS is scrollable on iOS

Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture .

Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

body-scroll-lock

  • 0 Dependencies
  • 843 Dependents
  • 89 Versions

Body scroll lock...just works with everything ;-)

Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a target element (eg. modal/lightbox/flyouts/nav-menus).

  • disables body scroll WITHOUT disabling scroll of a target element
  • works on iOS mobile/tablet (!!)
  • works on Android
  • works on Safari desktop
  • works on Chrome/Firefox
  • works with vanilla JS and frameworks such as React / Angular / VueJS
  • supports nested target elements (eg. a modal that appears on top of a flyout)
  • can reserve scrollbar width
  • -webkit-overflow-scrolling: touch still works

Aren't the alternative approaches sufficient?

  • the approach document.body.ontouchmove = (e) => { e.preventDefault(); return false; }; locks the body scroll, but ALSO locks the scroll of a target element (eg. modal).
  • the approach overflow: hidden on the body or html elements doesn't work for all browsers
  • the position: fixed approach causes the body scroll to reset
  • some approaches break inertia/momentum/rubber-band scrolling on iOS

LIGHT Package Size:

minzip size

You can also load via a <script src="lib/bodyScrollLock.js"></script> tag (refer to the lib folder).

Usage examples

React/es6 with refs.

In the html:

Then in the javascript:

Check out the demo, powered by Vercel.

  • https://bodyscrolllock.vercel.app for a basic example
  • https://bodyscrolllock-modal.vercel.app for an example with a modal.

reserveScrollBarGap

optional, default: false

If the overflow property of the body is set to hidden, the body widens by the width of the scrollbar. This produces an unpleasant flickering effect, especially on websites with centered content. If the reserveScrollBarGap option is set, this gap is filled by a padding-right on the body element. If disableBodyScroll is called for the last target element, or clearAllBodyScrollLocks is called, the padding-right is automatically reset to the previous value.

allowTouchMove

optional, default: undefined

To disable scrolling on iOS, disableBodyScroll prevents touchmove events. However, there are cases where you have called disableBodyScroll on an element, but its children still require touchmove events to function.

See below for 2 use cases:

More Complex

Javascript:

https://medium.com/jsdownunder/locking-body-scroll-for-all-devices-22def9615177 https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi

Refer to the releases page.

  • body scroll
  • body scroll lock
  • react scroll lock
  • react scroll

Package Sidebar

npm i [email protected]

Git github.com/willmcpo/body-scroll-lock

github.com/willmcpo/body-scroll-lock#readme

Unpacked Size

Total files, last publish.

3 years ago

Collaborators

willmcpo

New York, NY

Los Angeles, CA

Design & Development

Disable Body Scrolling For Open Modals on iOS Devices

When overflow: hidden just isn’t enough.

iPhone Rage / © chaffflare / Adobe Stock

Stacey Marks , Engineer

Feb. 18, 2020, the problem.

Are you creating a website that has a full page modal and want to stop pesky body background scrolling when the modal is open? Did you realize the method that pretty much works everywhere doesn’t work for Safari on iPhones?

Did you google how to fix this?

Did all of the answer pages point you to the body-scroll-lock package?

And when you tested the most recent version of it on a phone, it didn’t work?

THE SOLUTION

After scouring the internet for way too many hours, more than I care to admit, I found two solutions that worked for what we needed.

1. Stop everything from scrolling, both body background and everything inside modal. 2. Stop the background from scrolling, while allowing content inside the modal to scroll.

THE FIRST EXAMPLE - Freeze Everything

The first block of code is checking if we are indeed in Safari on an iPhone, otherwise we run what works for literally everything else.

The second block, we are checking if the modal is open, then run what’s needed to stop everything from scrolling.

Easy enough, right?

But wait! What if I actually need to be able to scroll inside my modal?

We got you covered.

safari body scroll lock

THE SECOND EXAMPLE - Allow Scrolling Inside Modal

For context, this is all in the modal component and only gets called once the modal is actually open. In addition,  the _.includes is from the Lodash library, which would have to be imported at the top of your component.

So, because the site is built using Gatsby, we had to take advantage of useEffect in order to access the DOM element to grab the document.

We grab #terms-text-container , which is the container that contains (ha) the modal’s text, not the entire modal itself. We are just targeting the div that we want to be able to scroll.

After checking if we are in mobile iOS Safari, we want to grab all the nodes (the text, in this case) inside #terms-text-container and add them to the insideTextModa l array, which you will see being used later. If we don’t do this and your finger hits a piece of text, it will register as the text <p> but wont register as #terms-text-container and disable scrolling.

Then we add the event listener. Because it's a phone scroll the action is touchmove and we call our method handleTouchMove below, which is defined just below.

Within handleTouchMove , if the event.target ( what our finger touches and moves) is either NOT the #terms-text-containe r, or anything inside of it, which is represented with the array insideTextModal , we preventDefault , aka stop it from scrolling

And once the modal is closed, we remove the touchmove event listener, and that’s it!

OTHER (PERSONAL) TAKEAWAYS

Removing Event Listeners In order to properly remove an event listener, what you pass to the removeEventListener must be the same as what you passed into the addEventListener .

QA’ing your iPhone Specific Work When testing iPhone specific situations like this, the issue may not be able to be replicated in the browser, even when using responsive mode. We were only able to see this issue when testing on an iPhone itself, or later, using the Xcode simulator, which was a life saver!!

Have a different way to solve this issue? Please let us know!

Prevent Page Scrolling When a Modal is Open

Avatar of Brad Wu

Please stop me if you’ve heard this one before. You open a modal, scroll through it, close it, and wind up somewhere else on the page than you were when you opened the modal.

That’s because modals are elements on a page just like any other. It may stay in place (assuming that’s what it’s meant to do) but the rest of page continues to behave as normal.

See the Pen Avoid body scrollable in safari when modal dialog shown by Geoff Graham ( @geoffgraham ) on CodePen .

Sometimes this is a non-issue, like screens that are the exact height of the viewport. Anything else, though, we’re looking at Scroll City. The good news is that we can prevent that with a sprinkle of CSS (and JavaScript) trickery.

Let’s start with something simple

We can make a huge dent to open-modal-page-scrolling ™ by setting the height of the entire body to the full height of the viewport and hiding vertical overflow when the modal is open:

That’s good and all, but if we’ve scrolled through the <body> element before opening the modal, we get a little horizontal reflow. The width of the viewport is expanded about 15 pixels more, which is exactly the with of the scroll bar.

Let’s adjust the right padding of the body a bit to avoid that.

Note that the modal needs to be shorter than the height of the viewport to make this work. Otherwise, the scroll bar on the body will be necessary.

Great, now what about mobile?

This solution works pretty great on desktop as well as Android Mobile. That said, Safari for iOS needs a little more love because the body still scrolls when a modal is open when tapping and moving about the touchscreen.

We can set the body to a fixed position as a workaround:

Works now! The body will not respond when the screen is touched. However, there’s still a “small” problem here. Let’s say the modal trigger is lower down the page and we click to open it up. Great! But now we’re automatically scrolled back up to the top of the screen, which is just as disorientating as the scrolling behavior we’re trying to resolve.

That’s why we’ve gotta turn to JavaScript

We can use JavaScript to avoid the touch event bubble. We all know there should be a backdrop layer when a modal is open. Unfortunately, stopPropagation is a little awkward with touch in iOS. But preventDefault works well. That means we have to add event listeners in every DOM node contained in the modal — not just on the backdrop or the modal box layer. The good news is, many JavaScript libraries can do this, including good ol’ jQuery.

Oh, and one more thing: What if we need scrolling inside the modal? We still have to trigger a response for a touch event, but when reaching the top or bottom of the modal, we still need to prevent bubbling. Seems very complex, so we’re not totally out of the woods here.

Let’s enhance the fixed body approach

This is what we were working with:

If we know the top of the scroll location and add it to our CSS, then the body will not scroll back to the top of the screen, so problem solved. We can use JavaScript for this by calculating the scroll top, and add that value to the body styles:

This works, but there’s still a little leakage here after the modal is closed. Specifically, it appears that the page already loses its scroll position when the modal is open and the body set to be fixed. So we have to retrieve the location. Let’s modify our JavaScript to account for that.

That does it! The body no longer scrolls when a modal is open and the scroll location is maintained both when the modal is open and when it is closed. Huzzah!

What about this:

Create a position: fixed; overflow: auto; overlay that expands to the window edge, and put the modal box inside that overlay.

Unfortunately not works on iOS safari, try this page, https://getbootstrap.com/docs/4.3/components/modal/

No mention of overscroll-behavior: contain? That should be the solution going forward in my opinion, even if support isn’t there yet.

Unfortunately overscroll-behavior: contain does not prevent the scrolling when the content is less or equal to the parent(as in, no overflow). If that would be possible(or fixed in browsers?) you could indeed just set the overscroll behavior on the modal and backdrop and all would be great and performant.

You just need to also set overflow: auto on the same element, or do any of a number of things that turn that element into a scrolling container.

Even your “huzzah” scrolls for me on Android Chrome.

May I know the details?

Thanks for sharing this article, Brad! We’ve used this technique on a few sites and I’ve been meaning to write about it for a while, but now you’ve saved me the trouble! Genuinely grateful for that.

Weirdly, on some projects this seems to perform between with a transform: translateY(...) , but it isn’t consistent. Sometimes that introduces a flicker, sometimes it’s noticeably faster. Just figured I’d share in case others would benefit from experimenting with the vertical offset property. ‍♂

I know transform: translateY(...) will break position: fixed when on transition. Do you mean that?

Genuine question: can you not just set overflow: hidden on the html element rather than the body and eliminate this issue completely? (besides the horizontal reflow for scrollbars)

I know it works in Firefox/Chrome/Edge/IE on Windows but haven’t checked Safari. I wouldn’t be surprised if Safari differs from the norm on this.

Not work on iOS safari, cannot avoid body scroll when modal is open, even body is covered by backdrop.

Seems Apple do something weird here, just like safari on mac doesn’t support <input type='date'/> (supports type, but no behavior), but safari on iOS does.

There’s a really light npm package that handles this quite well: https://www.npmjs.com/package/body-scroll-toggle

It essentially does exactly what you’ve described, but also keeps hold of the existing body styles before changing them.

Seems great. Thank you .

But what if I want the modal to be scrollable if it contains a lot of content?

Or just make the modal body scrollable and set a max-height.

I’ll bet you those 15px scrollbar width won’t work cross-browser. You mmmmmight want overflow:scroll instead, which will force an (always inactive, because you set the height) scroll bar, or do something based on calc(100vw – 100%) as per https://aykevl.nl/2014/09/fix-jumping-scrollbar

Yes, you are right. I’ve learn this 15px from bootstrap, it works on chrome and safari.

Good to see attention to a practical problem that shouldn’t be ignored. This solution also minds the Safari bars resizing and device rotation: https://radogado.github.io/natuive/#modal-window

position: fixed; for body element works fine on basic demo, but has performance issues in real life web pages on mobile. Learned this the hard way while working on fancybox.

There’s also the very popular Body scroll lock (BSL) npm package: https://github.com/willmcpo/body-scroll-lock

I’m not happy with your assumption that a scrollbar is even present. The default setting in macOS is that there simply is none; atleast none that’s occupying space and therefore doesn’t need a padding on the right.

David Walsh has a neat way to find out how much padding is needed! https://davidwalsh.name/detect-scrollbar-width

The key lies in someDiv.offsetWidth which gives you the width including vertical scrollbars. You then compare that to someDiv.clientWidth , which doesn’t include scrollbars, and voila, you know what you need to know!

Here is davids code from his article

Safari just fixed the scrolling body problem, so you shouldn’t need position: fixed anymore

https://twitter.com/vincentriemer/status/1136281635979030528

This comment made my day. Thanks!

On my iOS 13.5.1 phone the body still scrolls.

Overscroll behavior, while not 100% supported yet, is basically made for exactly this use case.

It stops any scrolling within an element from bubbling upward once the element has hit the end of it’s scrolling capacity.

https://caniuse.com/#feat=css-overscroll-behavior

overscroll-behavior: contain , that is.

It seems that the name of this variable needs to be “scrollY”, not “top”?

const top = document.body.style.top;

https://github.com/willmcpo/body-scroll-lock

Highly recommend.

Hi Brad! No “well actually” from me – just wanted to say thank you for this excellent write up. This article was extremely helpful in building an iOS-friendly modal component.

I came here to share this as well. Mobile Safari can cause some real scrolling headaches, especially when you have a modal that has scrolling and a background that has scrolling.

Related: https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi

And this is why I don’t use hacks… Thanks for the update.

I agree, overscroll-behavior: contain; is the answer to the problem. Everything else seems to be a hack.

Even if it’s not fully supported, and it doesn’t always apply (other comments noted edge cases).

Edge cases need a hack for wider support, but not for the optimal solution. In a few years, this page will be outdated and the actual answer needs to be the first thing demonstrated, not the hacks.

Hi Brad Wu! I use your code and fixed my problem. Special Thanks. :)

// When the modal is shown, we want a fixed body document.body.style.position = ‘fixed’; document.body.style.top = -${window.scrollY}px ;

On my Chrome (OSX) if you follow this sequence, position ‘fixed’ resets the scroll to 0. You first need to set the top value and then set the position ‘fixed’ otherwise your scroll goes to 0 before is being red from window.scrollY

Maybe this is already in the comments, but if the script is activated when the body is already fixed, it will jump the scroll to the top of the page. This was my case because I had nav options activating mega-dropdowns and this script.

Can one assist me with ImgMod: I wish to let it scroll, via mousewheel and not scrollbar, so with fixed body! I don’t succeed in it with the given examples. Thanks.

safari body scroll lock

Package detail

@zeix/body-scroll-lock.

safari body scroll lock

Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a target element (eg. modal/lightbox/flyouts/nav-menus)

Body scroll lock...just works with everything ;-)

Why body-scroll-lock ?

Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a target element (eg. modal/lightbox/flyouts/nav-menus).

  • disables body scroll WITHOUT disabling scroll of a target element
  • works on iOS mobile/tablet
  • works on Android
  • works on Safari desktop
  • works on Chrome/Firefox
  • works with vanilla JS and frameworks such as React / Angular
  • supports nested target elements (eg. a modal that appears on top of a flyout)
  • can reserve scrollbar width
  • -webkit-overflow-scrolling: touch still works

Aren't the alternative approaches sufficient?

  • the approach document.body.ontouchmove = (e) => { e.preventDefault; return false; }; locks the body scroll, but ALSO locks the scroll of a target element (eg. modal).
  • the approach overflow: hidden on the body or html elements doesn't work for all browsers
  • the position: fixed approach causes the body scroll to reset
  • some approaches break inertia/momentum/rubber-band scrolling on iOS

Usage examples

http://wp-os.s3-website-ap-southeast-2.amazonaws.com/body-scroll-lock-demo/index.html

On iOS mobile (as is visible in the above demo), if you scroll the body directly even when the scrolling is locked (on iOS), the body scrolls - this is not what this package solves. It solves the typical case where a modal overlays the screen, and scrolling within the modal never causes the body to scroll too (when the top or bottom within the modal has been reached).

reserveScrollBarGap

optional, default: false

If the overflow property of the body is set to hidden, the body widens by the width of the scrollbar. This produces an unpleasant flickering effect, especially on websites with centered content. If the reserveScrollBarGap option is set, this gap is filled by a padding-right on the body element. If disableBodyScroll is called for the last target element, or clearAllBodyScrollLocks is called, the padding-right is automatically reset to the previous value.

https://medium.com/jsdownunder/locking-body-scroll-for-all-devices-22def9615177 https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi

How To Prevent Scrolling The Page On iOS Safari 15

safari body scroll lock

If we show a modal on iOS we need to prevent events inside the modal from interacting with the page behind the modal. On a previous episode of “Fun with Safari” we could use preventDefault() on the "touchmove" event but on iOS 15 that no longer works. Here we go.

To make this work we need iOS to think that there is nothing to scroll.

But how do we do this?

We set the height of both the html and the body element to the window height and then set overflow on these elements to hidden so the content gets cut off.

For now it’s set to 100px .

A height of 100vh

A vh is a viewport height unit, 1vh means 1% of the viewport height.

Great. Let’s set the measure to 100vh and observe what happens.

Nope. This is not going to work. On iOS 100vh is always the full height of the viewport, even if the footer shows.

If you’re browsing this page on iOS Safari, scroll up and down a bit to see the height stays fixed no matter of the footer is active or not.

Using -webkit-fill-available

Let’s try the -webkit-fill-available property instead.

This is a -webkit- thing, so if you’re on Firefox the measure will now snap to 0

This will do the trick. But it results in a modal that is not filling up all available space.

-webkit-fill-available will fill the “safe” space, so it excludes the room the footer will take up if it’s active.

We want all the space.

It turns out that window.innerHeight reflects the actual available space. So what we want is this.

But you and I both know that this won’t fly because window.innerHeight is a JavaScript property.

Syncing window.innerHeight

Luckily we can use CSS Custom Properties to make this tick.

Now we switch to JavaScript land where we update the custom property when the window is resized.

The 'resize' event is trigged when the iOS Safari footer changes in height, so this nicely aligns our measure with the page height.

I know we could also “just” set measure.style.height but as we’re going to use Custom Properties for our modal in a minute so we might as well go the extra mile here.

If you’re on iOS, see if the measure scales correctly by scrolling up and down a bit.

We’re almost out of the woods. Our measure is now be the right size no matter if the iOS Safari footer is visible or not.

Now we’re going to apply this height to the html and body elements when our modal is activated. This prevents the page from scrolling.

We’ll add an is-locked class to our html element when we show the modal.

When the is-locked class is assigned we use it to set the required styles so the page cannot be scrolled.

Wait a minute, what is that calc() doing there?!

Let me try to explain. If the iOS Safari footer is hidden when the modal opens, iOS will reveal the footer if the user makes a vertical pan gesture, the 1px prevents this from happening.

I don’t know why, I just know that I’m so tired. 🤷‍♂️

Let’s just move on.

Now we listen for the 'resize' event to keep the --window-inner-height CSS Custom Property synced with the window.innerHeight in JavaScript land.

To prevent iOS from hiding the footer when the modal is visible and the footer is visible we need to prevent the default action of the 'pointermove' event.

If we want to support iOS 14 we need to add 'touchmove' as well.

We’re nearly done. 🫠

Because we’re resizing the document the user scroll position is lost when the modal opens. So we need to remember that position before it opens and restore it when the modal closes.

There we go! 😅

Try it out on iOS Safari 15 by clicking the “Open modal” button below. It’s been tested with the address bar at the bottom of the viewport (which is the default) and at the top. We can move it to the top by tapping “aA” and then “Show Top Address Bar”.

The modal is slightly transparent on purpose so we can see that the page is scrolled to the top. We can assign the backdrop-filter: blur(20px) style to the modal root element to make this less obvious.

I sincerely hope the Apple Safari dev team will fix this mess in the next upate, but I’m not holding my breath.

I share web dev tips on Twitter, if you found this interesting and want to learn more, follow me there

Or join my newsletter

Busy doing the newsletter subscribing.

Something went wrong, can you give it another go?

At PQINA I design and build highly polished web components.

Make sure to check out FilePond a free file upload component, and Pintura an image editor that works on every device.

Related articles

Typescript interface merging and extending modules, blocking navigation gestures on ios safari, styling password fields to get better dots.

Preventing body scroll for modals in iOS

Tl;dr solution for 2020/ios 13+.

Thanks to the comments below (and that’s why I love comments so much), we can put a few things together to have a working CSS only solution for iOS 13+:

You can have a play with that here: https://codepen.io/benfrain/live/wvayeWq

I have left the rest of the post un-touched from 2016.

Original post from 2016 follows

Suppose you are building something that pops a modal window from time to time. This probably works well in most places, the problem is, on iOS, even if you toggle:

iOS doesn’t prevent users from scrolling past the modal if there is content that exceeds the viewport height, despite you adding that condition to the CSS. One solution is to write the window.innerHeight into both HTML and body elements in the DOM and then toggle the overflow: hidden style on and off on the body and html :

You can get a similar effect by setting the body and html to position: fixed when you expose the modal and ditch the JavaScript there.However, neither of those solutions prevents users from doing the ‘elastic band’ thing at the bottom; and that subsequently reveals an ugly space. It would be nice if we had something better. My esteemed colleague, Tom suggested making use of touchstart to prevent the default scroll behaviour and while that solved the initial problem (being able to scroll past the modal) it prevented clicks inside the modal. But it wasn’t long before that approach led us to using touchmove instead.

We can use it like this — at the same time that you invoke a function to make the attribute change or class change that shows the modal, you also do this:

And when you want to allow scrolling again, you fire it like this:

Behind the scenes we then need two functions:

The function receives either true or false and subsequently adds or removes an event listener. We then need a simple function reference that disables the default behaviour of the touchmove event.

You can view a basic demo of this technique here: http://benfrain.com/playground/modal-demo.html . You’ll need to do further work if your modal contains a scrolling section but for basic modals this seems to solve the issue quite nicely.

I’d like a simpler CSS solution but this is a pretty light-weight way to get the job done. I welcome a better approach if anyone knows one?

34 comments:

Could you share details about your implementation? How is the dialog positioned in CSS? Is there an overlay? Do you use the standard element and API (with polyfill, ofc)? I’ve found a few demos of dialogs, but neither of them prevent body scroll, so I’d be interested to see a demo that does prevent it.

Hi Šime, I added a demo link at the bottom now. Thanks, Ben

Šime, not using standard API.

This is all well and good until the modal content is taller than the device window height. It prevents the scrolling of content in the modal. I know there is an argument that there shouldn’t be that much content in a modal, but sometimes there just is.

I think this is a fantastic solution, but maybe needs a little more expansion to account for this. I’ll definitely keep an eye on this, it’s extremely useful.

Goddamn it, I completely missed the part AFTER the demo. Sorry Ben, feel free to delete my comment.

Hey, did you find a suitable solution? My modal works fine with a overflow, but it’s not scrolling with -webkit-overflow-scrolling: touch; The scrolling without -webkit… feals really laggy and is a dealbreaker to me.

It still jumps on double tab below the modal :(.

I wish you didn’t point that out, otherwise this would’ve been the ultimate solution to this annoying issue.

I ran into something similar about a year ago when I was prototyping to improve the UX of elements on iOS. You can check out my solution here: https://jsejcksn.github.io/material-number-select.js/

I did a similar approach once but added one more thing to prevent the jump to top (I was using jQuery), right before adding the overflow: hidden, I store the scrollTop, the after the overflow, I add the scrollTop to the body. var scrollAmount = $(‘body’).scrollTop(); $(‘body’).css(‘overflow’,’hidden’).scrollTop(scrollAmount);

I also banged my head on this for quite some time, and I tried a LOT not only to prevent scrolling of the content underneath, but also to enable scrolling on the modal at the same time. My solution involves intercepting the touchmove event and cancel it when the modal is scrolled to the top or bottom. But still I have found no way to prevent it when you tap into those “magic” areas on the very top and bottom of the screen … you can see it in action on http://nordmedia.de when using the mobile navigation (yes, thats a modal as well)

Hi Frank. This is an amazing solution. I am trying to do a similar thing with a modal that has a scrollable text div in it. Could you share the basic code & CSS that solves this issue! I tried using ‘position:fixed;overflow:hidden’ on the parent window, but it still scrolls underneath the modal.

Hey!! Thanks for the tip. Works for me…just a little modification to the original solution posted here. Great Thanks again!!

There is a typo in the link to the webkit.orG bug for Touch-action css property support.

I checked your demo and unfortunately it does not work on Chrome for Android. Yes the post is about Safari iOS , I understand, just thought it might be OK for other people coming here to find out why it does not work there. It does indeed work on FF for Android.

The reason for that is that Chrome treats touch event as passive by default (to increase scrolling speed and scrolling tap response), FF for Android does not do this. https://www.chromestatus.com/feature/5093566007214080

Also https://developers.google.com/web/updates/2017/01/scrolling-intervention points out that, like you write, touch-action: none should be used and where this does not work, also, like you write correctly, the old method of preventing the default action for the event should be used in older browsers and mobile Safari (nice how they write older browsers and mobile Safari in one sentence – hint), though Chrome discourages that and rightfully so.

On another note, when I checked your site on mobile, in particular the navigation menu, (not sure if this is intended) the body does scroll up and down upon touch scrolling. Not a major thing though.

What really caught my attention though is that when I tap the search bar on mobile (FF for Android and Chrome for Android) the soft keyboard slides **over** the search input. This leaves the user with no way of seeing what they type to search for.

A simple solution to that would be to place the search bar at the top of your navigation menu and possibly have the Twitter, GitHub and RSS links show next to each other on one line instead of in a list under each other. Like this all menu items and the search bar could still be seen and interacted with without issues.

Btw, the email given for this comment works 100%, so if you like to get in touch feel free to do so, happy to hear back from you. I also subscribed to this entry so I will receive any replies.

Thank you for your write up! Well done!

Now I am left with the dilemma of not wanting to spend hard cash on Apple hardware or BrowserStack (live view is slow apparently) to test my sites in mobile Safari and desktop Safari, just to be able to debug this type of error or, like again you write so well in your new post, the hostile menu bar of iOS Safari.

To my knowledge there is no way to debug mobile or desktop Safari other than BrowserStack like tools or actually having Apple hardware, right? Yes of course a rouge Mac iso in a VM could do, been down that road but it is tedious and keeping that up to date is even more tedious plus the EULA is not happy with it. WHY does Apple make it so HARD for devs to write clean and 100% working sites for their trouble child called Safari? Bad move trying to even monetize development for their platform(s)!! Grrr!

I say, instead of putting AI chips in their latest phones and pads Apple should really get their act together and clean up their mobile and desktop browsing experience called Safari so us devs and users don’t have a heap of issues to go through to deliver a site that otherwise works great in all other major mobile and desktop browsers. Sigh..

Based on the post and other articles found on the internet, I have created a javascript function to solve this problem. I have tested it on Safari and Chrome on iOS and Chrome on Android. You can find a test case here: https://codepen.io/thuijssoon/pen/prwNjO and a gist here: https://gist.github.com/thuijssoon/fd238517b487a45ce78d8f7ddfa7fee9 . Please let me know your thoughts.

I struggled with a similar problem for 3 days, and the solution I found may help someone. My app has two main divs. The main one is for displaying pages and the other has the navigation links.

The main div allows vertical scrolling for overflowing content: overflow-y: scroll. Some pages have vertically scrollable widgets that don’t well, or are completely unusable, unless scrolling is temporarily disabled on the main content div.

Setting ‘overflow: hidden’ in css via a class didn’t stop the content div from scrolling because it was overridden by ‘overflow: scroll’ applied to the div using an id selector. I could see that this was happening using the element view in the Opera debugger. So I tried putting ‘overflow: hidden’ directly on the content div using the style attribute and it worked. Content area became unscrollable and scrollable widget worked perfectly. So I wrote a bit of javascript that wrote the css into the content div on touchstart and removed it on touchend. Job done. And working on every simulated iOS device I have tried so far.

See this fiddle for a working example. View in mobile browser.

its not working on iOS 12. Everything is scrolling 🙁

Do you mean my fiddle is not working 0lli0?

Confirm. Your demo is no longer working on iOS 12.1 Safari.

Your demo site is not working on iOS 12.1. Any other suggestion?

That’s a real bummer. Can I just check that you are looking at the right thing? (The original link seems to have gone from my message, so I am wondering…)

https://fiddle.jshell.net/daffinm/fwgnm7hs/16/show/

I am referring to modal on iOS. The demo site I tried is http://benfrain.com/playground/modal-demo.html The link you posted in your comment was about a veritcal slider, which was not what this article about, right? or do I misunderstand anything?

In my case I wanted to display a fixed-position modal fullscreen. And solution seems very simple: setting “overflow:hidden;” along with “height:100%;” on both html & body tags. The “height:100%;” is key here. That alone solves the scrolling problem, no javascript needed. Only modal content was scrollable, body was not scrolling at all. Tested on iPhone X Safari/Chrome, iPhone 8 Safari/Chrome, iPhone 7 Safari/Chrome (all via browserstack). Of course on Android Chrome it works flawlessly.

This did NOT solve the problem in my case. What i did was:

body.no-scroll, html.no-scroll { overflow: hidden; height: 100%; touch-action: none; }

The key seems to be “touch-action: none;” on iOS 12.1.x

You sir, are a genius. “touch-action: none;” does also solve it on iOS 13.

You also need to set “-webkit-overflow-scrolling: auto;”. if not set, scrolling to “negative” will invalidate “touch-action: none” and you will be able to scroll the body.

This no longer works on iOS 12

In my case i just had to add

overflow-x: scroll; -webkit-overflow-scrolling: touch;

To the modal only not to the body in order to be scrollable, without affecting the background content

This works, although for `-webkit-overflow-scrolling`, `none` is not a valid value. See https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-overflow-scrolling . Possible values are “auto” | “touch”

If like me, you spent hours trying to find a scroll solution for Safari 10, I found it and made a simple React hooks version here: https://codesandbox.io/s/react-modal-scrollable-safari-10-friendly-3xzzy

2021 update with iOS 14+ All solutions mentioned are not working as soon as the browser bars in Safari disappear. So if you already scrolled the page and then open a modal, the scroll event will bubble as soon as you reach the top or bottom of your modal scroll. This is also an issue if you have a PWA in standalone mode added to the homescreen where there are no browser bars at all, and therefore no way to block this behaviour.

I made a demo to replicate the issue. It even has a manifest.json so you can add it like a PWA in standalone mode to your homescreen.

https://5n944.csb.app/

This is so dumb, I also noticed that my modal works perfectly when I set the url bar to be at the top, and nothing helps when it’s at the bottom.

This helped me on Dec 22, 2023 thank youuuuuuu

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.

Understanding native JavaScript array methods

The ios safari menu bar is hostile to web apps: discuss.

Search code, repositories, users, issues, pull requests...

Provide feedback.

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly.

To see all available qualifiers, see our documentation .

  • Notifications

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement . We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a Safari/WebKit bug for this? #211

@foolip

foolip commented Jan 12, 2021

  • 👍 3 reactions
  • 🎉 5 reactions

@brunostasse

brunostasse commented Jan 16, 2021 • edited

  • 👍 5 reactions

Sorry, something went wrong.

foolip commented Jan 22, 2021

@diachedelic

diachedelic commented Jan 27, 2021

Brunostasse commented jan 27, 2021 • edited.

  • 👍 2 reactions

@ryanbadger

ryanbadger commented Feb 16, 2021

@connor-baer

No branches or pull requests

@diachedelic

IMAGES

  1. iOS Safari 13 · Issue #134 · willmcpo/body-scroll-lock · GitHub

    safari body scroll lock

  2. Scroll Lock Key: What Is It & How To Turn It Off

    safari body scroll lock

  3. How to lock Safari private tabs in iOS 17 and macOS Sonoma

    safari body scroll lock

  4. Scroll Lock on a Mac

    safari body scroll lock

  5. [Solved] IOS Safari: unwanted scroll when keyboard is

    safari body scroll lock

  6. Scroll Lock Key: What Is It & How To Turn It Off

    safari body scroll lock

VIDEO

  1. How to press Scroll Lock

  2. NEW fragrances I'm completely OBSESSED WITH...(these are all 10/10's)

  3. Safari trolley bag ka lock 🔐🔒 kaisey forget kare

  4. Safari Bag Lock password Reset || Trolley Bags Password Lost || Bag Lock Repair || Assemble Lock

  5. Quick Scroll

  6. safari bag lock forgot l safari trolley bag lock code forgot #safaritrollybag #safaribag #unlock

COMMENTS

  1. javascript

    I want the body element on iOS 13 Safari to not scroll. This means no scrolling, and no elastic bounce (overflow-scrolling) effect. I have two elements next to each other on which I have set overflow: scroll;, those should scroll, just the body around them shouldn't.

  2. GitHub

    Features: disables body scroll WITHOUT disabling scroll of a target element. works on iOS mobile/tablet (!!) works on Android. works on Safari desktop. works on Chrome/Firefox. works with vanilla JS and frameworks such as React / Angular / VueJS. supports nested target elements (eg. a modal that appears on top of a flyout) can reserve scrollbar ...

  3. Locking `body` scroll for modals on iOS

    The sledgehammer: listening for touch move. Libraries such as Body Scroll Lock selectively block touchmove events using JavaScript. This mostly works, but feels very heavy-handed. You also need to know up front which (if any) root element you want to retain scrolling on, such as the inner content of the modal itself.

  4. Simple Solution to Prevent Body Scrolling on iOS

    As you can see above, we use position: fixed in combination with storing the scroll position of the user so we can restore the scroll position after the fact.. Caveats. There are certainly some downsides to this approach. If you change the size of the browser window while the scroll lock is active, for example, the scroll position does not get restored correctly.

  5. body-scroll-lock

    Latest version: 4.0.0-beta.0, last published: 3 years ago. Start using body-scroll-lock in your project by running `npm i body-scroll-lock`. There are 839 other projects in the npm registry using body-scroll-lock. Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a ...

  6. Prevent body scrolling for safari/chrome

    The content in the dialog/popup is scroll-able. Maintain the scroll position of the content in the background. Make it work to prevent body scrolling. Having tested with most suggested methods found and experiment around a bit, luckily I am able to land on a method using css and simple javascript. Live demo to lock body scroll

  7. How to fight the <body> scroll

    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 ...

  8. tuax/tua-body-scroll-lock

    TargetElement needs scrolling(iOS only). In some scenarios, when scrolling is prohibited, some elements still need to scroll, at this point, pass the targetElement. import { lock, unlock } from 'tua-body-scroll-lock' const elementOne = document.querySelector('#elementOne') const elementTwo = document.querySelector('#elementTwo') // one ...

  9. Body scroll lock

    OK, let's try approach #2. 2. Prevent Default. Great solution — scrolling is blocked! But it also blocks scrolling inside the targetElement. If content in the targetElement has height ...

  10. rick-liruixin/body-scroll-lock-upgrade

    // 1. Import the functions import {disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks,} from 'body-scroll-lock-upgrade'; class SomeComponent extends React. Component {// 2. Initialise your ref and targetElement here targetRef = React. createRef (); targetElement = null; componentDidMount {// 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox ...

  11. body-scroll-lock

    the approach document.body.ontouchmove = (e) => { e.preventDefault(); return false; }; locks the body scroll, but ALSO locks the scroll of a target element (eg. modal). the approach overflow: hidden on the body or html elements doesn't work for all browsers; the position: fixed approach causes the body scroll to reset

  12. body-scroll-lock

    Latest version: 4.0.0-beta.0, last published: 2 years ago. Start using body-scroll-lock in your project by running `npm i body-scroll-lock`. There are 825 other projects in the npm registry using body-scroll-lock. Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a ...

  13. Disable Body Scrolling For Open Modals on iOS Devices

    1. Stop everything from scrolling, both body background and everything inside modal. 2. Stop the background from scrolling, while allowing content inside the modal to scroll. THE FIRST EXAMPLE - Freeze Everything . The first block of code is checking if we are indeed in Safari on an iPhone, otherwise we run what works for literally everything else.

  14. Prevent Page Scrolling When a Modal is Open

    The width of the viewport is expanded about 15 pixels more, which is exactly the with of the scroll bar. See the Pen Avoid body scrollable in safari when modal dialog shown by Geoff Graham (@geoffgraham) on CodePen. Let's adjust the right padding of the body a bit to avoid that.

  15. @zeix/body-scroll-lock

    the approach document.body.ontouchmove = (e) => { e.preventDefault; return false; }; locks the body scroll, but ALSO locks the scroll of a target element (eg. modal). the approach overflow: hidden on the body or html elements doesn't work for all browsers; the position: fixed approach causes the body scroll to reset

  16. Safari toolbars on iOS 15 show and hide when scrolling within a

    Testing the modal example on iOS 15 safari. Even though the body is locked in scroll position Safaris toolbars disappear / re-appear when scrolling inside a scrollable element. See video: https://u...

  17. How to disable body scrolling when modal is open IOS only

    Step 2: Simulate the previous scroll distance while the modal is open. Bootstrap exposes events that fire when a modal is opened or closed. We can use these to solve the "jump to the top" issue by pulling the top of the body up when a modal is opened, so that it looks like the scroll position hasn't changed:

  18. How To Prevent Scrolling The Page On iOS Safari 15

    window.scrollTo(0, scrollY); } There we go! 😅. Try it out on iOS Safari 15 by clicking the "Open modal" button below. It's been tested with the address bar at the bottom of the viewport (which is the default) and at the top. We can move it to the top by tapping "aA" and then "Show Top Address Bar".

  19. Locking the body scroll, blocks target element scrolling

    tua-body-scroll-lock has the same functions that body-scroll-lock provides. Like. disableBodyScroll alias for lock; enableBodyScroll alias for unlock; clearAllBodyScrollLocks alias for clearBodyLocks; I made a fiddle with an example using tua-body-scroll-lock. Your code should then be like. import {lock, unlock, clearBodyLocks} from 'tua-body-scroll-lock'; const NavigationIcons = (props) => { ...

  20. Preventing body scroll for modals in iOS

    html, body { overflow: hidden; } iOS doesn't prevent users from scrolling past the modal if there is content that exceeds the viewport height, despite you adding that condition to the CSS. One solution is to write the window.innerHeight into both HTML and body elements in the DOM and then toggle the overflow: hidden style on and off on the ...

  21. Is there a Safari/WebKit bug for this? #211

    The difference between WebKit on iOS and all other browser engines is that it doesn't allow to reliably prevent body scrolling by setting overflow: hidden to the body, as it should. This is a very (very) long-standing issue with WebKit on iOS. Setting overflow: hidden to the body used not to prevent body scrolling in any situation before ...