Safe areas on iPhone: notch, Dynamic Island and env()
What iPhone safe areas are, when Safari handles them for you, and how to use viewport-fit=cover with env(safe-area-inset-*) without breaking layouts.
A safe area is the part of the screen your content can use without being covered by the notch or Dynamic Island, the rounded corners or the home indicator bar. By default Safari keeps your page inside it, so most sites need no extra code at all. If you opt out with viewport-fit=cover, keeping content clear becomes your job: pad it with env(safe-area-inset-top), -right, -bottom and -left, usually inside max() or calc() so ordinary phones keep their normal padding.
What's in the way on an iPhone
Every Face ID iPhone has rounded screen corners and a home indicator bar along the bottom edge. On top of that, each model has one of three things at the top:
| Top of the screen | iPhones |
|---|---|
| Dynamic Island | iPhone 14 Pro and Pro Max, 15 / 15 Pro, 15 Pro Max / Plus, 16, 16 Plus, 16 Pro, 16 Pro Max, 17, 17 Pro, 17 Pro Max, Air, 18 Pro, 18 Pro Max |
| Notch | iPhone 11 / XR, 13 mini / 12 mini, 14 / 13 / 12, 14 Plus / 13 Pro Max / 12 Pro Max, 16e, 17e |
| Nothing (home button) | iPhone SE (2nd & 3rd gen) |
The iPhone Duo is its own case, with a system strip down one side of the outer screen; see iPhone Duo for web developers. Every model is listed on the iPhone hub.
In portrait, the notch or island sits in the status bar area and the home indicator sits under Safari's bottom toolbar, and Safari handles both for you. Turn the phone sideways and it's a different story: the cutout lands on the left or right edge, exactly where your text starts, and the home indicator runs along the new bottom edge.
What Safari does by default
Out of the box, the viewport tag's viewport-fit is auto. In landscape, Safari then lays your page out inside the safe area and fills the strips next to the cutout with your page's background colour. WebKit's original write-up, Designing Websites for iPhone X, shows it well.
Two side effects:
- Your page is narrower than the screen on its side. An iPhone 17 Pro is 874px wide in landscape, but the width your CSS sees is 874 minus the two side strips. That matters when a breakpoint like 768 sits close by; Landscape on phones covers what that does to your layout.
- Full-width colours stop short. A dark header or a photo band ends before the edges, and the strips show the page background instead. That letterboxed look is the usual reason people reach for
viewport-fit=cover.
If neither bothers you, you're done. Nothing on this page is required.
Opting out: viewport-fit=cover
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
Now the page lays out to the full size of the screen, edge to edge, and the cutout and home indicator can sit on top of your content. In Next.js you set this with viewportFit: 'cover' in the viewport export rather than a hand-written tag; the details are in The meta viewport tag.
Only add cover in the same change as the padding below. On its own it just invites text under the Dynamic Island.
The four env() variables
env() reads a value the browser provides. The safe-area ones are:
| Variable | Non-zero when | Typical use |
|---|---|---|
env(safe-area-inset-top) |
the page runs under the status bar area or a cutout at the top | headers in full-screen, app-style layouts |
env(safe-area-inset-right) and env(safe-area-inset-left) |
the phone is on its side: both sides get an inset, not only the one with the cutout | side padding of full-width sections |
env(safe-area-inset-bottom) |
the page reaches the home indicator or Android's gesture bar | fixed bottom bars, the last section of the page |
Two rules from MDN's env() page make them safe to use everywhere. The values are 0 when the viewport is a plain rectangle with nothing in the way. And they change as the visible area changes, for example when the browser's bars slide in or out. So never hard-code a pixel value "for iPhones": read the variable and let the browser keep it current.
Fallbacks
env() takes an optional second value, used when the variable doesn't exist:
padding-bottom: env(safe-area-inset-bottom, 0px);
MDN lists env() as widely available since 2020 and every current browser knows the four safe-area variables, so that fallback mostly matters for newer ones. For browsers too old to understand env() at all, write a plain line first. A browser that can't parse the second line throws it away and keeps the first:
.site-header {
padding-left: 16px;
padding-right: 16px;
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
}
max() or calc()?
They do different jobs, and picking the wrong one is the most common safe-area bug.
max(16px, env(safe-area-inset-left))uses whichever is bigger. Right for side padding: on a normal phone you get your 16px, and next to a cutout the inset replaces it. You don't need 16px of padding stacked on top of a strip that's already clear of the island.calc(12px + env(safe-area-inset-bottom))adds them. Right for bottom bars: your 12px of breathing room sits on top of the space the home indicator needs, so buttons never touch it.
Landscape: keeping text off the sides
With cover on, the pattern for any full-width section is "background to the edges, text inside the safe area":
.band {
background: #111;
color: #fff;
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
}
Put this on every element that sets its own side padding (header, footer, full-bleed sections), not only on one wrapper. Text inside a card within a padded container is already safe. There's more on side padding, including how doesitfit's TIGHT sticker measures it, in Text touching the screen edge?.
Fixed bottom bars and the home indicator
A position: fixed element is placed against the viewport, so padding on body or a wrapper doesn't move it. The inset has to go on the bar itself:
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 12px max(16px, env(safe-area-inset-right)) calc(12px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
}
/* Stop the bar covering the end of the page */
body {
padding-bottom: calc(72px + env(safe-area-inset-bottom));
}
That padding shorthand is top, right, bottom, left, so the bar handles the home indicator and landscape cutouts in one go. How that bar behaves as Safari's and Chrome's toolbars move, and when the keyboard opens, is in Fixed headers and bottom bars on mobile.
Android: the same CSS, two extra cases
Chrome on Android uses the same opt-in and the same variables.
- Camera cutouts. Chrome adds margin so your page stays clear of a punch-hole or notch, and since Chrome 69
viewport-fit=coverplusenv(safe-area-inset-*)works as it does on iPhone. Test portrait and landscape. - The gesture bar. From Chrome 135, the viewport extends into Android's gesture navigation bar (Chrome's edge-to-edge guide). Without
cover, a "chin" sits over the gesture bar and moves out of the way as you scroll, like the address bar, andsafe-area-inset-bottomupdates live as it moves. Withcover, there's no chin: the page reaches the bottom edge from the first load.
Chrome 135 also added env(safe-area-max-inset-bottom), the largest value the bottom inset will reach. It stays constant, so you can size a bottom bar with it and avoid the bar changing height mid-scroll. Chrome's guide recommends that over padding a fixed bar with safe-area-inset-bottom, which it says causes layout thrashing and stops the chin sliding away. It's newer than the other variables, so give it a fallback, as in Chrome's guide.
Common mistakes
viewport-fit=coverwith no padding. Headings end up under the Dynamic Island in landscape and buttons under the home indicator.- Replacing padding with the inset.
padding-left: env(safe-area-inset-left)is 0 on most phones and in portrait, so text touches the edge. Wrap it:max(16px, env(safe-area-inset-left)). - Padding the wrong element. Fixed and absolutely positioned bars ignore their parent's padding. Put the inset on the bar.
- Hard-coded numbers. A fixed
padding-bottomin pixels "for the iPhone home bar" is wrong on the SE, on Android, in landscape and whenever the bars move. - Handling top and bottom only. Left and right are the ones that bite, and only when the phone is on its side.
- Adding the inset twice. If the header already includes
env(safe-area-inset-top), don't add it to thebodyas well. - Only
constant(). That was the first iOS 11 spelling. Current code should useenv().
Check it
A throwaway rule outlines the safe area so you can see it:
/* Temporary: draws the safe area as a red box */
body::after {
content: "";
position: fixed;
inset: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
outline: 2px solid red;
pointer-events: none;
z-index: 9999;
}
Load the page on a real iPhone, portrait then landscape, scrolled and not. Anything outside the red box can be covered. How to open localhost on your phone gets your dev build onto it.
In doesitfit, turn on Mockups to draw the real phone frames, notches and Dynamic Islands, then press Rotate to see where the cutout lands on every iPhone at once. One honest limit: previews run in your desktop browser, which has no cutout, so env() values there are 0 and your safe-area padding only shows up on a phone. The mockups show where the danger zones are; the phone confirms your padding clears them.
Does your site fit?
Paste your address (or localhost:3000). doesitfit shows it on real screen sizes at once and tells you, bluntly, where it spills.
Free, no sign-up. Or ask Claude, ChatGPT or Codex to check it.
A prompt for your AI tool
Check how my site handles iPhone safe areas. If the meta viewport tag (or the Next.js
viewportexport) includesviewport-fit=cover, then: give every header, footer and full-width sectionpadding-left: max(16px, env(safe-area-inset-left))andpadding-right: max(16px, env(safe-area-inset-right)); give every fixed bottom barpadding-bottom: calc(12px + env(safe-area-inset-bottom))on the bar itself; add matchingpadding-bottomto the page so the bar doesn't cover the last content; and if any header runs under the status bar, addenv(safe-area-inset-top)to its top padding once. Replace any hard-coded notch or home-indicator pixel values and anyconstant()calls withenv(). Ifviewport-fit=coverisn't used and nothing needs to run edge to edge, leave it out. Show me each change.
The short version
- The safe area is the part of the screen nothing covers. Safari keeps you inside it unless you add
viewport-fit=cover. - With
cover, usemax(16px, env(safe-area-inset-left/right))for sides andcalc(12px + env(safe-area-inset-bottom))for bottom bars. - The insets are 0 where nothing is in the way and change as toolbars move, so use them everywhere and never hard-code them.
- Put the inset on the element that needs it, especially fixed bars.
- Test landscape: that's where the notch and Dynamic Island meet your text.
Questions people ask
What is env(safe-area-inset-bottom) on iPhone?
It's the amount of space at the bottom of the viewport that the home indicator (or another system feature) needs. It's 0 when nothing is in the way, so you can add it to your padding on every device.
Why does env(safe-area-inset-top) come out as 0 on my iPhone?
Usually because your page never reaches a cutout. Without viewport-fit=cover Safari keeps the page inside the safe area by itself, and in portrait Safari's own bars usually cover the top and bottom. Add viewport-fit=cover and turn the phone sideways to see non-zero values.
Do I need viewport-fit=cover?
Only if you want your backgrounds to run under the notch, Dynamic Island and home indicator, or you're building an app-style layout with a bottom bar. If you add it, you also have to add safe-area padding.
Does safe-area-inset work on Android?
Yes. Chrome on Android has supported viewport-fit=cover and the safe-area variables for camera cutouts since Chrome 69, and since Chrome 135 safe-area-inset-bottom also covers the gesture navigation bar.
What's the difference between constant() and env()?
constant() was an early spelling WebKit shipped in iOS 11 and then replaced with the standard env(). Use env(); you only need constant() if you still care about very old iPhones.