Managing full-screen elements on iOS devices

Safari's handling of the viewport creates some interesting side-effects for UIs leveraging full-height or bottom-aligned elements.

Since iOS 7, Safari on iPhone has had collapsible toolbars, which scale or hide as a user scrolls down a page, increasing the viewport height and making more content visible. This is a great feature well executed for the most part, giving back valuable screen real estate on smaller devices, which is especially nice given the recent proliferation of bottom toolbars and sharing buttons cough…Medium…cough.

However, there is a problem with it: elements styled with CSS vh or vw units will change their height / width – also changing any values computed based on them – each time the toolbars are hidden or shown.

This is not necessarily a problem, so much as a constraint, something to consider when styling elements. Here are some of the cases I’ve come up against this, and how I’ve solved it.

Full-height images

Say we’ve got a full-screen image, maybe in a gallery, or as a background applied to a full-height element, styled with background-size: cover; or object-fit: cover; As the viewport changes, the dimensions of the image or container also change, zooming it in or out.

// CSS

.img {
    width:            100vw;
    height:            100vh;
    object-fit:        cover;

A solution: fixed aspect ratios

Instead of relying on the viewport to dictate the dimensions of our image, we can give it a minimum height or a fixed aspect ratio.

There are two ways two reliably calculate a fluid height relative to a width: using vw units, or a pseudo-element width a top padding:

// Sass

.ratio--16-9 {
    width:        100vw;
    height:        56.25vw;
    // because 100 / 16 * 9 = 56.25

.ratio--2-3 {
    width:        100vw;

    &:after {
        content:    "";
        display:    block;
        position:    absolute;
        top:        0;
        width:        100%;
        padding:    150%;
        // because 100 / 2 * 3 = 150

The first solution works for elements sized with viewport units, but when we don’t know the computed width, we’ll get inconsistent results.

// Haml

// Sass

.wrapper {
    width:        100vw;
    padding:    0 1.5rem;

.ratio--4-3 {
    width:        100%;
    height:        75vw;

In this case, the image width at 100% is not equal to 100vw, so our fixed height will be greater than intended. We could of course use calc() to compensate for the padding, but the formula would get quite complicated when we nesting things. So I prefer to use the pseudo-element solution, which relies on the fact that percentage-based padding values are always calculated from the width of an element.

Fixed position elements

For UI elements aligned to the bottom of the viewport, I would usually do the following:

// Sass

nav {
    position:    fixed;
    width:        100vw;
    bottom:        0;

There are two problems with this:

  1. the navigation moves with the bottom of the viewport – users should be able to trust that important UI elements and navigation are consistently positioned.
  2. if a link is within 44px of the bottom of the viewport, tapping it will bring the Safari toolbar back up – and also prevent the tap from activating the link, requiring a repeated attempt, by which time the navigation has moved, possibly prompting a third attempt, and… well, this isn’t great.

So what I do is instead align the nav to the top, and offset it by a px height, which I hard-code based on the specific size of the iOS status bar and Safari toolbars.

Unfortunately, this is a bit of a hack, as we will now have to give a specific offset to each size of iPhone, which I guess is okay, because, it’s not like we have to worry about device fragmentation, right?