Ian J MacIntosh.com

Mobile-Friendly Footers

I just wanted to put a footer at the bottom of the screen on desktop and mobile, but getting it right was surprisingly kind of a pain in the butt.

Even though the page content was way shorter than the screen, the footer got pushed "below the fold" on mobile. On Safari for iOS, I could kinda see the footer behind the address bar.

See the Demo

Screenshot of a hidden footer on iOS

Screenshot of an inexplicably hidden footer on mobile (Safari 18 on iOS 18.1.1; Apple iPhone 14 Pro)

Shippable? You bet. Perfect? Nope.

If you're here just looking for the fix:

body {
  min-height: 100dvh;
}

See the Fixed Demo

What I was missing was a unit of measure that takes the browser chrome into account: dvh

“In a browser, the chrome is any visible aspect of a browser aside from the webpages themselves (e.g., toolbars, menu bar, tabs). This is not to be confused with the Google Chrome browser”

"Dynamic Viewport-Percentage Units" like dvh are defined by the height of the viewport minus the chrome, so authors can fit content within the exact dimensions of the viewport — whether things like address bars and toolbars are visible or not.

When I searched Google for solutions, I found a couple of other things that aren't quite right:

This article you're reading is the one I wish I'd found before wasting my time with those other solutions.

Attempt #1: CSS grid (rows: 1fr auto) and min-height: vh100

I started out super simple. A document body with two children:

HTML

<body>
  <div class="container">...</div>
  <footer>...</footer>
</body>

And I added a couple of simple CSS rules:

CSS

body {
  background: #fff;
  color: #000;
  display: grid;
  grid-template-rows: 1fr auto;
  min-height: 100vh;
}

footer {
  background: #000;
  color: #fff;
  text-align: center;
}

See the Demo

That produced this:

Screenshot of a badly styled page on MacOS

Here's a screenshot of a weird-looking footer (Brave 1.73.91 on macOS Sequoia 15.0; MacBook Air 15-inch, M3, 2024)

I saw I forgot a few things:

No problem, I can fix that.

Attempt #2: Remove the margin, add padding, apply box-sizing everywhere

The solutions here are old as heck in the world of front-end developers:

CSS

* {
  box-sizing: border-box;
}

body {
  ...
  margin: 0;
}

footer {
 ...
 padding: 10px;
}

See the Demo

These results made the page look way better on my computer, but when I checked on my phone, something was pushing the footer down out of view, despite the fact that the page was practically empty.

Here's a video of me scrolling up and down to show and hide the footer on my phone (Brave 1.73.91 on Android 15; Google Pixel 7)

Even worse, I couldn't reproduce the problem on my desktop, so testing a fix would require a real phone or an emulator.

I did some Googling and found a fix that involved introducing some exotic syntax I'd never encountered: env(safe-area-inset-bottom)

Attempt #3: padding-bottom: calc(10px + env(safe-area-inset-bottom))

The env() CSS function allows CSS authors to access "environment variables" set in the browser itself. The syntax in your spreadsheet would look like: env(name-of-variable-goes-here) MDN Docs

One such environment variable is safe-area-inset-bottom

That sounded perfect, so I gave it a shot.

I figured I could just pad the bottom of the footer with that value, plus the normal 10px I wanted anyway:

CSS

footer {
  ...
  padding-bottom: calc(10px + env(safe-area-inset-bottom))
}

See the Demo

Which yields...

Here's a video of me scrolling up and down to show and hide the footer on my phone (Brave 1.73.91 on Android 15; Google Pixel 7)

...no change.

You'll probably be tempted to stop reading right about now. If you do, you won't learn why that's the wrong tool for this problem.

The term "safe area" refers to the parts of a display that will be visible on a non-rectangular display, like the 2017 Apple iPhone X one with rounded corners. Those corners of the image are going to get cut off, which could be a problem if there's something important there.

For example: Picture a little tiny "x" (close button) in the top right corner of a full-screen advertisement on an iPhone X. If that button was small enough and close enough to the top right corner of the image, it'd get cut off entirely and you'd never be able to click it.

To let CSS authors know what parts of the screen will be obscured by the screen shape, WebKit for iOS provides those boundaries through its safe-area-inset-* environment variables.

safe-area-inset-bottom is the wrong fix because it refers to display visibility, not page visibility.

But I didn't read the documentation, so I kept on going down the same path, assuming I had a minor syntax issue. After searching Google some more, I found something else related: viewport-fit=cover

Attempt #4: Update the meta viewport tag with viewport-fit=cover

All this safe-area-inset-* stuff is about covering the whole screen, which I'm not sure mobile browsers do by default. Maybe they're allocating space at the bottom of the screen for system navigation buttons or browser controls.

So another recommendation I found was to update the <meta name="viewport" /> tag with viewport-fit=cover to make sure the screen gets covered. The WebKit team even made a 2017 blog post about it.

HTML

<meta
  name="viewport"
  content="width=device-width, initial-scale=1, viewport-fit=cover"
/>

See the Demo

And this change yields...

Here's a video of me scrolling up and down to show and hide the footer on my phone (Brave 1.73.91 on Android 15; Google Pixel 7)

...no difference.

After researching more, I'm not sure that syntax above is valid. The CSS Viewport Module Level 1 spec makes no mention of it, and searching for viewport-fit on caniuse yields 0 results.

There's an old pull request for CSS Round Display Level 1 describing safe-area-inset-* that mentions viewport-fit could be set to cover, but doesn't say how that should be set.

The current spec itself says it's set with CSS.

CSS

@viewport {
  viewport-fit: cover;
}

Back to the actual problem at hand: none of this fixes it. My page scrolls when it doesn't need to, and my footer isn't visible on page load.

Then I found another solution: set the page height using dvh units — a unit of measure that considers browser chrome.

Attempt #5: Use min-height: 100dvh

For years, I've set my document's min-height using viewport-percentage units, where 100vh represents 100% of the viewport height.

But what exactly is the viewport height?

Paraphrasing the spec, viewport size could mean 1 of 3 different things:

  1. Large viewport: The largest potential viewport area, assuming all dynamically expanding and contracting browser UI elements like toolbars and address bars are retracted
  2. Small viewport: The smallest potential viewport area, assuming all browser UI elements are extended
  3. Dynamic viewport: Sized with "dynamic consideration" of browser UI elements. This lets authors size their content so it can exactly fit in the viewport, whether or not those interfaces are visible. Ding ding ding ding ding!!

By default, calling vh or vw refers to the large viewport, but my mobile browser initially loads the page in a small viewport. The spec explicitly warns about this when using large viewport-percentage units:

“This allows authors to size content such that it is guaranteed to fill the viewport, noting that such content might be hidden behind such interfaces when they are expanded.”

I want the page to fill the viewport's actual size when I'm looking at it, not its potential full size. This must be computed dynamically, which can be done using dynamic viewport-percentage units: 100dvh:

CSS

footer {
    ...
    min-height: 100dvh;
}

See the Demo

And that yields...

Here's a video of me trying to scroll up and down on my phone, but I can't because the content exactly fits the viewport (Brave 1.73.91 on Android 15; Google Pixel 7)

...exactly what I want!

I don't know why I didn't come across this solution first; it's clearly the most logical one, 93.8% of users' browsers support it and has been widely supported since 2022. But I'd never heard of it.

Conclusion

min-height: 100dvh is my new standard for setting a page's height.

I'd like to say that the next time I run into a problem, I'll take a measured approach and read specifications instead of copying and pasting random "fixes" from Google, but I'm not so sure about that. I found dvh by jumping around all mimbledy-bimbledy from search result to search result.

A more realistic takeaway is that if I'm spinning my wheels, whenever I'm ready to calm down, those scary specification documents aren't as stuffy as they look. Like the joke goes, a few hours of trial and error can save you from wasting 20 minutes reading the docs.