CSS · · 5 min read

Four Lines of CSS and My Phone Finally Called Me Out

I was building a fixed bottom nav and it looked fine in DevTools. On a real phone, the home indicator was sitting right on top of the buttons. The fix was four lines of CSS I'd been skipping for years.

I was working on a fixed bottom navigation bar last week. Standard stuff. Position fixed, bottom zero, full width, some padding, looks great. Chrome DevTools confirmed it. I confirmed it. We were all in agreement.

Then I pulled it up on my phone.

The home indicator was sitting right on top of my nav buttons. Not overlapping in a dramatic, obvious way. Just enough to make tapping the bottom row feel unreliable. The kind of thing you'd blame on your thumb before you'd blame on your CSS.

I'd seen env(safe-area-inset-bottom) in other people's code before and never really stopped to understand what it was doing. It looked like one of those defensive CSS things that careful people add, like box-sizing: border-box or -webkit- prefixes. Important in theory, easy to skip in practice.

Turns out I'd been skipping something that actually matters.

What I Didn't Know

Modern phones have all kinds of stuff encroaching on the viewport. Notches, Dynamic Islands, rounded corners, home indicators. The browser knows exactly where these obstructions are, and CSS gives you four environment variables to ask about them:

env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left)

Each one returns the distance from that edge of the screen to the nearest unobstructed area. Simple enough. But here's the part I missed: they don't do anything unless you opt in.

The Opt-In I'd Been Skipping

By default, the browser just insets your entire page so nothing overlaps with device UI. Safe, but you lose screen real estate and the env() values all return 0. To actually use them, you need this in your viewport meta tag:

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

That viewport-fit=cover tells the browser to let your layout extend to the physical edges. Now you're responsible for keeping content out of the danger zones. And now the env() values actually have numbers in them.

The Fix That Made Me Feel Silly

Once I understood the mechanism, the actual fix was almost embarrassingly simple:

.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding-bottom: calc(0.75rem + env(safe-area-inset-bottom));
  padding-left: calc(1rem + env(safe-area-inset-left));
  padding-right: calc(1rem + env(safe-area-inset-right));
}

The calc() part is key. You don't want just the inset. You want your normal spacing plus whatever the device needs. On a phone with a home indicator, that's 0.75rem + 34px or whatever the device reports. On desktop, it's 0.75rem + 0. One declaration, every device. No media queries, no JavaScript detection, no device-specific overrides.

I went back and applied the same pattern to a sticky header I'd built the week before:

.top-bar {
  position: sticky;
  top: 0;
  padding-top: calc(0.5rem + env(safe-area-inset-top));
}

Same idea. And suddenly the layout on my phone looked like I'd actually tested it there. Which, to be fair, I should have been doing all along.

A Few Things I Learned the Hard Way

The values change when you rotate the phone. In portrait, the top and bottom insets do the heavy lifting. Flip to landscape and the notch moves to the side, so safe-area-inset-left or safe-area-inset-right suddenly has a value. If you have fixed side panels or drawers, they need the same treatment.

Webviews are a whole separate situation. If your site runs inside a native app wrapper, the webview configuration determines whether viewport-fit=cover is even honored. I haven't hit this one yet on my current project, but I've read enough horror stories to know it's coming.

The env() function takes a fallback. You can write env(safe-area-inset-bottom, 0px) if you're worried about older browser support. At this point it's more useful as documentation than as an actual safety net. Browser support is solid.

The viewport meta tag needs to be in the HTML, not injected by JavaScript. If you're dynamically adding it, you might get inconsistent behavior on first paint. Just put it in the <head> and forget about it.

What I'm Taking Away

The thing that got me about safe area insets is how well they fit into the way CSS has been evolving. The browser knows things about the device that I don't. Instead of writing JavaScript to detect notches and apply classes, I just ask the browser to tell me where the obstructions are and build my spacing around that.

It's the same idea behind prefers-color-scheme, prefers-reduced-motion, container queries. Describe what you want, let the platform sort out the specifics.

I spent maybe twenty minutes total understanding and implementing this. I'm a little annoyed I didn't do it sooner. If you've got any fixed or sticky elements in your current project, go check them on a phone with a notch. You might be surprised at what you find.