Whether you're developing a web application with native-ish UI, or just a simple modal popup overlay that covers the viewport, when it comes to making things work on iDevices in Mobile Safari, you're in for a decent amount of pain and suffering. Making something "100% height" is not as easy as it seems.
This post is a collection of Mobile Safari's gotchas and quirks on that topic, some with solutions and fixes, some without, in good parts pulled from various sources across the internets, to have it all in one place. Things discussed here apply to iOS8, iOS9 and iOS10.
Screen real estate on smartphones is limited, so Mobile Safari collapses the browser chrome (address bar and optional tab bar at the top, and tool bar at the bottom) when the user scrolls down. When you want to make something span exactly the height of the viewport, or pin something to the bottom of the screen, this can get tricky because the viewport changes size (or not, as we see later).
Additional care must be taken when text input controls are involved. The virtual keyboard will pop up when a text input is focused, shifting elements up (so they are guaranteed to stay in view), potentially messing with the layout when certain conditions are met.
To create an overlay that covers the entire viewport, you'd apply CSS like the following to the element:
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%; // or right: 0;
height: 100%; // or bottom: 0;
}
Make sure that no ancestor element of .overlay
have a transform
applied to them (a simple translateX(0)
is sufficient). From CSS Transforms Module:
Any computed value other than
none
for the transform results in the creation of both a stacking context and a containing block. The object acts as a containing block for fixed positioned descendants.
So if positioning turns out not to be relative to the viewport, you probably have a transform
applied on an ancestor, and your element is positioned relative to that ancestor's bounds.
Why not use 100vh
you ask?
vh
stands for "Viewport Height", one vh
unit reflects 1/100th of the total height of the browser's viewport. An element with 100vh
applied is supposed to have the same height as the viewport, independent of heights applied to parent elements. The perfect way to force full viewport height on an element you might think.
Not in Mobile Safari though, where vh
is always relative to the viewport size with collapsed browser chrome, no matter if the browser chrome is actually collapsed or fully visible, so an element with height: 100vh
will be too big when the chrome is fully visible.
Giving the element a height: 100%
fixes this (the computed height will adjust to viewport size changes due to toggled browser chrome etc), and bottom: 0
works just as well.
Now we could almost wrap up here if there weren't any quirks, and of course there are.
On iPhone in landscape mode, with no tab bar visible, things start getting a little weird. On page load, Mobile Safari starts with an incorrect window.innerHeight
value (e.g., 414 on a 6 Plus, which is the full viewport height, same as if height: 100vh
was used). After the first render with this incorrect height, window.innerHeight
is set to the correct value (e.g., 370 on a 6 Plus), a reflow happens and a resize event is fired. Collapsing and expanding the browser chrome via scrolling also triggers reflows and resize events.
On iPhone in landscape mode, with the tab bar visible (only "Plus" models), things are hopelessly broken. It starts with the same incorrect window.innerHeight
value, but no reflow happens and no resize event is fired. Collapsing and expanding the browser chrome via scrolling does not trigger reflows or resize events either. window.innerHeight
is updated with the correct values though. Additionally, the touch areas of interactive elements (buttons and such) are offset vertically by a seemingly random amount. See this Stackoverflow question and Webkit bug #153056. The only way to get around this is querying window.innerHeight
repeatedly (e.g., on touch events), and setting an explicit px
height on the element.
Note that this does not happen on iPhone in portrait orientation, or on iPad in both orientations: Everything works as it should, window.innerHeight
has the correct value from the start, reflows happen and resize events are fired.
If you have a text field in your fixed positioned element, and it is positioned at or near the bottom, when it's focused and the keyboard pops up, the scrollable ancestor scrolls all the way down for some reason. This StackOverflow answer shows how to fix it.
Hidden Treasures
Last but not least, if you have a tappable element (like a button) at the very bottom of the viewport, and the browser chrome is hidden, you're in for a surprise: In portrait mode, taps on the area where the bottom tool bar would be trigger the browser chrome to show (just like taps on the collapsed address bar), so your button will initially not be tappable. The only solution to that is to force browser chrome to always be visible. See this article for details.
Normally, setting overflow: hidden
on the body
element prevents it from scrolling. That doesn't work in Mobile Safari though (see Webkit bug #153852).
The only way to prevent body
from scrolling via CSS is to set position: fixed
on body
, which results in some undesirable side effects:
- If the page was scrolled down previously, it jumps back to the top.
- In portrait mode, if the browser chrome was previously collapsed, it expands, and can not be toggled by vertical swipes.
- In landscape mode, the browser chrome stays in its previous state, but can be toggled by vertical swipes.
This can mostly be remedied by setting top
to -scrollTop
(plus potentially some delta adjusting for the jump caused by the expanding browser chrome), and afterwards resetting scrollTop
to its original value. But bugs in landscape mode with visible tab bar outlined above still apply, and make this non trivial in that particular configuration.
-
Keep site content in a container component:
body { position: fixed; } .container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: auto; -webkit-overflow-scrolling: touch; }
This forces the browser chrome to be always visible and never collapse.
-webkit-overflow-scrolling: touch
is applied to enable rubber band scrolling on that element. Setoverflow: hidden
on.container
to disable scrolling. -
If the overlay does not need any scroll- or other gestures, blocking the
touchmove
event on the overlay viapreventDefault()
does the trick:[...document.querySelectorAll('.overlay')].forEach(overlay => overlay.addEventListener('touchmove', event => { event.preventDefault(); }) );
Also read the article Six things I learnt about iOS Safari's rubber band scrolling, which explains quite well how to block overscroll amongst other things.
Sick hack to make
overflow: hidden
work with fixed positioned elements in Mobile Safari: Twohtml
tags and twobody
tags