Safe Area Insets: Designing Around Notches, Corners and Home Indicators

Phones no longer have rectangular screens. Camera cut-outs intrude from the top edge, display corners are rounded, and gesture bars sit across the bottom. By default, mobile browsers protect pages from all of this by drawing them inside a rectangle that avoids those areas, leaving the corners filled with the browser's own background. That is safe but wastes space and breaks edge-to-edge designs. Opting into full-screen drawing means taking responsibility for keeping text and buttons out of the unsafe zones. This page shows the opt-in, the env() variables that report the unsafe areas, and the max() pattern that makes padding correct on every device. It belongs to Viewport Units & Mobile Layout within the Mastering Container Queries & Responsive Layouts guide.

Why the default is not enough

The default letterboxing is conservative on purpose: it guarantees nothing important ends up under a notch, at the cost of visible bars at the screen edges in landscape and a background colour that stops short of the corners. For a text-heavy page that is fine. For a full-bleed hero image, a coloured app header that should extend under the status bar, a map, or an installed web app that should feel native, the bars look broken.

Opting out of the letterbox with viewport-fit=cover makes the page's background extend to every physical edge. It also means the browser stops moving content away from the unsafe areas, so a fixed header's title can now sit under the camera cut-out, and a bottom navigation bar's buttons can sit under the home indicator where taps are interpreted as a system gesture. The safe area inset variables exist to let you put spacing back exactly where it is needed.

This is an accessibility issue as much as a visual one. Buttons under the home indicator are hard or impossible to activate, and text under a notch is unreadable, so a page that opts into edge-to-edge drawing and forgets the insets fails basic operability on exactly the devices it was trying to serve better.

What the insets describe

The four safe area insets in landscape A landscape phone outline with a camera notch on the left edge and a home indicator along the bottom. The left inset is large to clear the notch, the right inset matches it for symmetry, the bottom inset clears the indicator, and the top inset is zero. Insets measure the unsafe margins, per edge safe area text and controls go here left right bottom env(safe-area-inset-left | right | top | bottom)

The browser exposes four environment variables: safe-area-inset-top, -right, -bottom and -left. Each is a length measuring how far that edge's unsafe region extends into the viewport. They are read with env(), which works like var() but for values supplied by the user agent instead of the stylesheet, and which accepts a fallback: env(safe-area-inset-left, 0px).

The values change with orientation. In portrait on a notched phone, the top inset is large and the sides are zero. Rotate to landscape and the notch moves to a side, so the left or right inset becomes large and the top drops to zero. Some browsers apply the same inset to both sides in landscape so centred layouts stay symmetrical; do not assume which side the notch is on.

Insets by orientation In portrait, the top inset clears the camera and the bottom inset clears the home indicator while the sides are zero. In landscape, the left and right insets clear the camera, the bottom inset clears the indicator, and the top is zero. Rotation moves the large inset to a different edge Portrait Landscape top large (camera) top 0 left / right 0 left / right large (camera) bottom indicator bottom indicator Write rules for all four edges; never special-case an orientation.

Because the values are live, rules written with env() update automatically on rotation, with no orientation media query required. Writing all four edges once is both simpler and more robust than guessing which edge a given device will need.

The complete implementation

The page below opts into edge-to-edge drawing, lets the header and footer backgrounds reach the physical edges, and keeps their contents inside the safe area.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- viewport-fit=cover: draw under notches and indicators. Without it,
     every safe-area inset reports 0 because the browser letterboxes. -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Safe area insets</title>
<style>
  html, body { margin: 0; }
  body {
    font: 16px/1.6 system-ui, sans-serif;
    background: #f8fafc;
  }

  /* Spacing tokens the design would use on a normal rectangle screen. */
  :root { --pad: 1rem; }

  /* The header background runs under the status bar and notch; its
     content is pushed inward by whichever is larger: the design padding
     or the inset on that edge. */
  .app-header {
    position: sticky;
    top: 0;
    background: #1e3a8a;
    color: #ffffff;
    padding-top: max(var(--pad), env(safe-area-inset-top));
    padding-right: max(var(--pad), env(safe-area-inset-right));
    padding-bottom: var(--pad);
    padding-left: max(var(--pad), env(safe-area-inset-left));
  }

  /* Body content respects side insets in landscape. */
  main {
    padding-inline: max(var(--pad), env(safe-area-inset-left))
                    max(var(--pad), env(safe-area-inset-right));
  }

  /* The tab bar is fixed to the bottom; its buttons must clear the
     home indicator, so the bottom inset is added below the buttons. */
  .tab-bar {
    position: fixed;
    inset: auto 0 0 0;
    display: flex;
    justify-content: space-around;
    background: #ffffff;
    border-top: 1px solid #e2e8f0;
    padding-top: 0.5rem;
    padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
    padding-inline: env(safe-area-inset-left) env(safe-area-inset-right);
  }
  .tab-bar a {
    min-width: 44px;
    min-height: 44px;
    display: grid;
    place-items: center;
    color: #1e3a8a;
  }

  /* Leave room at the end of the page for the fixed tab bar. */
  main { padding-bottom: calc(4rem + env(safe-area-inset-bottom, 0px)); }
</style>
</head>
<body>
  <header class="app-header"><h1>Trips</h1></header>
  <main><p>Content…</p></main>
  <nav class="tab-bar" aria-label="Sections">
    <a href="/">Home</a><a href="/trips/">Trips</a><a href="/profile/">Profile</a>
  </nav>
</body>
</html>

Three details are easy to miss. The logical shorthand padding-inline in main still takes physical insets, because the insets are physical by nature — a notch on the left is on the left regardless of text direction — so in right-to-left text the values should be swapped or written with physical longhands. The tab bar's links are 44 by 44 pixels, the minimum target size recommended in Target Size and Pointer Accessibility, and the inset adds space below them rather than shrinking them. And the final main padding uses calc() intentionally: the tab bar's height plus the inset is genuinely additive, unlike the edge padding.

The key technique: max(), not calc()

The single most common mistake is adding the inset to the existing padding. On a phone whose bottom inset is 34 pixels, calc(1rem + env(safe-area-inset-bottom)) gives 50 pixels of padding — the design's spacing plus the indicator's, stacked. The design only ever needed one of them.

calc() stacks the spacing, max() chooses it On a device with no inset both give 16 pixels. On a device with a 34 pixel inset, calc gives 50 pixels while max gives 34 pixels, which is just enough to clear the indicator. Bottom padding with a 1rem design value no inset (0px) indicator (34px) calc(1rem + inset) 16px 50px, too much max(1rem, inset) 16px 34px, just right The inset already includes breathing room around the indicator; adding design padding on top double-counts it.

max() expresses the actual requirement: "at least my design padding, and at least enough to clear the hardware". On an ordinary screen the inset is zero and the design value wins. On a notched screen the inset wins where it is larger and the design value wins where it is not. There is no device on which the spacing is too small or doubled.

Use calc() only where two distances really are additive, as with the page-end padding that must clear both the tab bar's height and the indicator beneath it.

Variation: safe insets in an installed web app

An installed progressive web app with "display": "standalone" or "fullscreen" in its manifest has no browser toolbar at all, so the page reaches the bottom edge permanently and the bottom inset is always in effect on phones with a home indicator. The same rules apply, but two more considerations come in: the status bar at the top is drawn over the page, and its text colour is decided by the theme-color meta tag, so the header's background must contrast with it.

/* In standalone mode, extend the header colour under the status bar
   and make sure the header is tall enough to hold both. */
@media (display-mode: standalone) {
  .app-header {
    padding-top: max(1rem, env(safe-area-inset-top));
    min-height: calc(3.5rem + env(safe-area-inset-top, 0px));
  }
}

/* When a landscape notch is present, keep long horizontal content
   such as carousels from scrolling under it. */
.carousel {
  scroll-padding-inline: max(1rem, env(safe-area-inset-left))
                         max(1rem, env(safe-area-inset-right));
}

The carousel rule is a reminder that insets are useful beyond padding. scroll-padding decides where snapped items come to rest, and without the inset the first card of a scroll-snap carousel would snap into position behind the notch. For full-width media that should run under the insets while text does not, the approach in Full-Bleed Sections in Constrained Layouts pairs naturally with these rules.

Browser support

env() with the four safe-area-inset-* variables is supported in all current mobile browsers, and desktop browsers report zero for each, so the rules are safe to ship unconditionally. max() is supported in Chrome and Edge 79+, Firefox 75+ and Safari 13.1+. The viewport-fit=cover meta key is honoured by browsers on devices with non-rectangular displays and ignored elsewhere. Always pass a fallback to env() inside calc()env(safe-area-inset-bottom, 0px) — because an unresolved env() without one makes the whole declaration invalid at computed-value time.

FAQ

Why does env(safe-area-inset-top) always return 0? Because the page has not opted into drawing under the unsafe areas. Without viewport-fit=cover in the viewport meta tag, the browser letterboxes the page inside the safe area itself, so every inset is zero. Add viewport-fit=cover and the insets report real values on devices that have them.

Should I add the inset to my padding or replace the padding with it? Neither exactly. Use max(normal padding, the inset) so the element keeps its designed spacing on ordinary screens and grows only where the inset is larger. Adding them with calc() doubles the spacing on devices where the inset is already generous.

Do safe area insets matter on desktop? Rarely. On desktop browsers the insets are zero, so rules using them fall back to the normal padding. They matter on phones and tablets with notches, camera cut-outs, rounded display corners or a home indicator, and in installed web apps that draw edge to edge.

Does the bottom inset change when the browser toolbar appears? It can. On phones with a home indicator, the bottom inset is non-zero when the page reaches the bottom edge of the screen, which happens in full-screen web apps and when the browser toolbar is hidden. Fixed bottom bars should always include it so their buttons never sit under the indicator.

Related articles

More pages in the same section.

002F",1789717747224]