CSS env() and Safe Area Insets: iPhone Notch, Dynamic Island & PWA
AI generated
CSS · iPhone · Notch · Dynamic Island · PWA
CSS env() and Safe Area Insets
iPhone Notch, Dynamic Island and PWA Fullscreen

Ever since the iPhone X with its notch and the iPhone 14 Pro with its Dynamic Island, web developers have had to make sure content does not disappear behind hardware elements. CSS env() together with the safe-area-inset-* variables is the standardized solution, working the same way across browsers, PWAs and native WebViews.

10 min read env() · safe-area-inset · viewport-fit · PWA · Dynamic Island iOS 11+ · Safari · Chrome · Firefox

1. Why safe area insets exist

With the iPhone X, Apple introduced its first iPhone without a home button and with rounded display corners back in 2017. The display extends across almost the entire front of the device, interrupted only by the notch, the cutout for the camera and Face ID sensors. This design decision presented web developers with a new challenge: content that reached into the corners or under the notch was hidden behind hardware elements. The solution was CSS env() together with the safe-area-inset variables, which Apple initially introduced as a proprietary solution and which were later adopted into the CSS specification.

The principle is simple: when a website or PWA switches into fullscreen mode and thereby uses the entire screen, including the areas behind the notch and Dynamic Island, the operating system supplies distance values that indicate how far the "safe" area is from each screen edge. These values depend on the device and the orientation: in landscape mode the notch shifts to one side, producing different inset values than in portrait mode. CSS safe-area-inset-* makes these values available in CSS without requiring JavaScript or native code.

2. viewport-fit=cover as the starting point

Without a special setting in the viewport meta tag, Safari automatically adds margins on devices with a notch so that content does not extend under hardware elements. This default behavior is convenient, but it means the page has white margins on the sides and does not use the full screen space. Anyone who wants to use that space has to add viewport-fit=cover to the viewport meta tag. This attribute instructs the browser to expand the viewport to the entire screen, including the areas behind the notch and Dynamic Island.

Once viewport-fit=cover is active, the safe-area-inset-* variables are populated with correct values. Without this attribute, the inset values are always 0, because the browser is already handling spacing on its own. This makes it important to note: CSS env(safe-area-inset-*) only makes sense in combination with viewport-fit=cover. Anyone who sets viewport-fit=cover but forgets the env() variables ends up with a layout where content sits behind hardware elements, which is the most common implementation mistake.


/*
 * Step 1: Enable viewport-fit=cover in HTML:
 * <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
 *
 * Step 2: Use env() variables in CSS
 */

/* Basic safe area padding, all four sides */
.full-bleed-layout {
  padding-top: env(safe-area-inset-top);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
}

/* Shorthand with logical properties */
.app-shell {
  padding-block-start: env(safe-area-inset-top);
  padding-block-end: env(safe-area-inset-bottom);
  padding-inline-start: env(safe-area-inset-left);
  padding-inline-end: env(safe-area-inset-right);
}

/* Old syntax for compatibility (iOS 11.0 - 11.1) */
.legacy-support {
  padding-top: constant(safe-area-inset-top);
  padding-top: env(safe-area-inset-top);
}

3. The CSS env() function

The env() function in CSS works similarly to var() for custom properties, but it accesses environment variables defined by the user agent (browser or OS) instead of self-declared CSS variables. The syntax is identical: env(variable-name, fallback). The fallback value is optional and is used when the environment variable is not available, for example on desktop devices without a notch, or in browsers that do not support env().

One important difference from var(): CSS env() can be used anywhere in a CSS value, including inside calc() expressions. The specification defines a set of predefined environment variables, of which the safe-area-inset-* variables are the best known. Browsers can implement additional ones, for example for various UI elements. The variables are specified in the coordinate system of the screen, not relative to the viewport origin.

4. The four safe-area-inset directions

There are four safe-area-inset environment variables: safe-area-inset-top, safe-area-inset-right, safe-area-inset-bottom, safe-area-inset-left. In portrait orientation on the iPhone X, safe-area-inset-top is roughly 44 pixels (the notch height), safe-area-inset-bottom is roughly 34 pixels (the home indicator area), and the side insets are zero. In landscape orientation the notch shifts to one side, so safe-area-inset-left or safe-area-inset-right gets a value while safe-area-inset-top becomes smaller.

For fixed elements such as navigation bars, FABs (floating action buttons) and bottom navigation bars, these values are critical. A fixed header needs padding-top: env(safe-area-inset-top) so the text does not extend under the notch. A bottom bar needs to set padding-bottom: env(safe-area-inset-bottom) so content stays above the home indicator area. Without these adjustments, interactive elements become inaccessible or visually obscured by hardware overlays.


/* Fixed header respecting notch and status bar */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;

  /* Combine existing padding with safe area inset */
  padding-top: calc(1rem + env(safe-area-inset-top, 0px));
  padding-left: calc(1.5rem + env(safe-area-inset-left, 0px));
  padding-right: calc(1.5rem + env(safe-area-inset-right, 0px));

  background: white;
  border-bottom: 1px solid rgb(226 232 240);
}

/* Bottom navigation bar, above home indicator */
.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;

  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);

  background: white;
  border-top: 1px solid rgb(226 232 240);
}

/* Floating action button, safe from all notch areas */
.fab {
  position: fixed;
  bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
  right: calc(1.5rem + env(safe-area-inset-right, 0px));
}

5. Combining env() with calc()

Combining CSS env() with calc() is the core of a correct safe area implementation. It is rarely correct to simply set padding: env(safe-area-inset-top): most of the time an element needs its own padding, to which the safe area inset still needs to be added. The pattern calc(1rem + env(safe-area-inset-bottom, 0px)) adds 16 pixels of its own padding to the inset value. The fallback value 0px in env() matters here: on devices without safe area support, env() without a fallback returns an invalid value, which invalidates the entire calc() calculation.

Another useful pattern is using CSS custom properties as an intermediate layer for safe area values. By writing the env() values into custom properties, they can be used flexibly across components without referencing safe-area-inset directly everywhere. This pattern is especially helpful in design system contexts and in Tailwind-based projects where the values are meant to be used as tokens.


/* CSS custom properties as safe-area tokens */
:root {
  --sat: env(safe-area-inset-top, 0px);
  --sar: env(safe-area-inset-right, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
}

/* Use tokens in components */
.page-content {
  /* Content starts below header + notch */
  padding-top: calc(var(--header-height, 4rem) + var(--sat));
  /* Content above bottom nav + home indicator */
  padding-bottom: calc(var(--bottom-nav-height, 4rem) + var(--sab));
  /* Side padding respects landscape notch */
  padding-left: max(1.5rem, calc(1.5rem + var(--sal)));
  padding-right: max(1.5rem, calc(1.5rem + var(--sar)));
}

/* Scrollable content with safe area at bottom */
.scroll-container {
  overflow-y: auto;
  /* Reserve space at the bottom for safe area within scroll */
  scroll-padding-bottom: var(--sab);
}

/* Safe padding shorthand using CSS logical properties */
.card-full-bleed {
  padding: var(--sat) var(--sar) var(--sab) var(--sal);
}

6. PWA fullscreen and standalone mode

Progressive Web Apps in standalone mode behave like native apps: they have no browser chrome, no address bar, and they use the entire screen. In this mode, CSS safe-area-inset-* variables become especially important because the browser no longer inserts an automatic margin. Without correct inset handling, the PWA content extends directly into the notch or under the home indicator area.

The web app manifest must set "display": "standalone" or "display": "fullscreen". In fullscreen mode, viewport-fit=cover is implicitly active and safe-area-inset-* returns correct values. In standalone mode it is also active, but the browser status bar may still be visible depending on the device. The CSS media query @media (display-mode: standalone) makes it possible to apply safe area styles only in PWA mode and use different values for browser display.

7. Dynamic Island and newer iPhone models

The Dynamic Island, introduced with the iPhone 14 Pro, is technically a software extension of the physical hardware cutout for the front camera. From a CSS perspective it behaves similarly to the notch: safe-area-inset-top has a value that ensures content does not extend behind the Dynamic Island. The concrete pixel value is somewhat larger on these devices than on notch iPhones, but the implementation principle stays identical.

One important aspect: the Dynamic Island is interactive and can expand when system features such as timers, calls, or Live Activities are active. In that state it takes up more space, and the safe area inset value grows accordingly. Because CSS env() updates dynamically, a correctly implemented layout adapts automatically, with no extra code needed. This is a significant advantage over hardcoded pixel values or JavaScript-based detection.

8. Safe area insets compared

Different approaches to handling the notch and Dynamic Island each have their own advantages and drawbacks. The following table compares the most important methods.

Approach Dynamic Device independent Recommendation
CSS env() safe-area-inset-* Yes Yes Primary solution, the standard
Fixed pixel margins No No Not recommended, breaks on new models
JS window.screen detection Limited Limited Last resort for legacy code
viewport-fit=auto (default) n/a Yes When no fullscreen layout is needed
Native app WebView Yes Yes Same as browser, env() works identically

For completeness: CSS env() is not limited to safe-area-inset-*. The specification defines further variables such as keyboard-inset-* (for the virtual keyboard), which are available in modern browsers. The foundation stays the same: enable viewport-fit=cover, then work with env() and calc().

9. Tailwind CSS integration

In Tailwind-based projects, CSS safe-area-inset-* values can be made accessible through custom theme extensions. In Tailwind CSS v4 with its CSS-first approach, utility classes are defined directly in CSS. The pattern is to set env(safe-area-inset-*) as a custom property on :root and then use it through CSS variables inside Tailwind utilities. Alternatively, many Tailwind plugins such as tailwind-safe-area offer ready-made utility classes for safe area padding.

In Hyva Themes projects using Tailwind CSS v4, the recommended approach is to declare safe area tokens in the global CSS file and then combine them in template code using inline styles or custom utility classes. Since Hyva uses Alpine.js, safe area values can also be read reactively through JavaScript and exposed as Alpine data, which is helpful for complex layout calculations but unnecessarily complex for simple padding use cases.


/* Tailwind v4 CSS-first safe area utilities */
@layer utilities {
  .pt-safe {
    padding-top: env(safe-area-inset-top, 0px);
  }
  .pr-safe {
    padding-right: env(safe-area-inset-right, 0px);
  }
  .pb-safe {
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .pl-safe {
    padding-left: env(safe-area-inset-left, 0px);
  }
  .p-safe {
    padding-top: env(safe-area-inset-top, 0px);
    padding-right: env(safe-area-inset-right, 0px);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    padding-left: env(safe-area-inset-left, 0px);
  }

  /* Additive: combine with existing padding */
  .pb-safe-4 {
    padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  .pb-safe-6 {
    padding-bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
  }
}

/* Media query: apply only in standalone PWA mode */
@media (display-mode: standalone) {
  .pwa-header {
    padding-top: env(safe-area-inset-top, 0px);
  }
}

Mironsoft

Mobile-first CSS, PWA development and iOS layout optimization

Need layouts that work correctly on iPhone notch and Dynamic Island?

We implement correct safe area handling for websites and PWAs, from viewport-fit=cover through env() integration to Tailwind utility classes for all four inset directions.

iOS audit

Checking all fixed elements for correct safe area handling on current iPhone models

PWA optimization

Fullscreen PWA with correct safe area insets in standalone and fullscreen mode

CSS system

Anchoring Tailwind utilities and CSS tokens for safe area values in your design system

10. Summary

CSS env() together with the safe-area-inset-* environment variables is the standardized and recommended method for displaying layouts correctly on devices with a notch, Dynamic Island, or rounded display corners. The implementation follows a clear pattern: enable viewport-fit=cover in the viewport meta tag, then read safe-area-inset-top, -right, -bottom and -left with env() and combine them with your own padding inside calc() expressions. Fallbacks in env(variable-name, 0px) protect devices without safe area support.

Fixed elements such as headers, bottom navigation and floating action buttons deserve special attention, since they are the most directly affected by the notch and Dynamic Island. For PWAs in standalone mode, correct safe area implementation is not optional but necessary to achieve native app quality. Combining CSS env() with CSS custom properties as an intermediate layer, plus an optional Tailwind utility extension, results in a maintainable, cross-device system.

CSS env() and Safe Area Insets: The Essentials at a Glance

Prerequisite

Enable viewport-fit=cover in the meta tag. Without it, all safe-area-inset-* values are always 0.

Four directions

safe-area-inset-top, -right, -bottom, -left. Orientation dependent: different values in landscape than in portrait.

calc() combination

calc(1rem + env(safe-area-inset-bottom, 0px)), your own padding plus inset. The 0px fallback in env() is mandatory.

Dynamic Island

Behaves like the notch for CSS. safe-area-inset-top adjusts automatically, even when the Dynamic Island is expanded.

11. FAQ: CSS env() and Safe Area Insets

1What is CSS env() and what is it used for?
A CSS function for browser environment variables. The most important are safe-area-inset-*, which supply distances for the notch and Dynamic Island.
2Do I have to set viewport-fit=cover?
Yes: without viewport-fit=cover all safe-area-inset-* values are always 0 and env() has no effect.
3viewport-fit=cover without env(), what happens?
Content ends up under the notch or Dynamic Island, the most common implementation mistake. viewport-fit=cover makes the problem visible but does not fix it.
4How do I combine env() with my own padding?
calc(1rem + env(safe-area-inset-bottom, 0px)), the 0px fallback in env() is mandatory for a correct calc() calculation.
5Does env() also work on Android?
Yes, Chrome on Android supports env() for devices with display cutouts. Without a cutout, inset values are 0.
6Testing without a physical device?
Safari DevTools with an iPhone simulator, or Chrome DevTools Device Toolbar. Alternatively, set custom properties temporarily to fixed values.
7What is constant() and do I still need it?
The old syntax from iOS 11.0 to 11.1. Current projects only need env(). For iOS 11.0 support, specify both: constant(...); env(...);
8Does the inset change when the Dynamic Island expands?
Yes, safe-area-inset-top grows when the Dynamic Island is expanded. CSS env() updates dynamically, so layouts adapt automatically.
9Reading env() values in JavaScript?
Write the value into a CSS custom property first, then read it via getComputedStyle. env() is not directly accessible in JS.
10Are device-specific CSS values needed?
No, env() automatically supplies device-dependent values. A single CSS implementation works across all devices and orientations.