Skip to main content
MSH Logo

Understanding env() Safe Area Insets in CSS

Published on

Modern devices come with notches, rounded corners, home indicators, and dynamic islands that can obscure your content. The CSS env() function with safe area insets ensures your layouts adapt to these hardware features automatically.

The Problem: Hardware Intrusions

When the iPhone X introduced the notch in 2017, it created a new challenge for web developers. Content that looked perfect on rectangular screens suddenly got hidden behind hardware features. Today, this problem extends to:

  • Notches and Dynamic Islands (iPhone, many Android devices)
  • Rounded display corners (most modern smartphones)
  • Home indicators (gesture-based navigation bars)
  • Camera punch-holes (Samsung, Google Pixel)
Why This Matters

Without safe area handling, fixed headers can hide behind notches, and bottom navigation can conflict with home indicators, making parts of your app unusable.

Enabling Safe Area Insets

By default, browsers letterbox your content to avoid hardware intrusions. To access the full screen and handle safe areas yourself, add the viewport-fit meta tag:

index.html
1<meta
2  name="viewport"
3  content="width=device-width, initial-scale=1, viewport-fit=cover"
4/>

The viewport-fit property accepts three values:

ValueBehavior
autoDefault letterboxing, content stays in safe area
containSame as auto, explicit safe area containment
coverContent extends to full screen, you handle safe areas
Important

Setting viewport-fit=cover without handling safe areas will cause content to be hidden behind hardware features. Always combine it with env() insets.

The env() Function

The env() function retrieves environment variables defined by the user agent. For safe areas, browsers provide four variables:

style.css
1/* Safe area environment variables */
2env(safe-area-inset-top)    /* Top edge (notch, status bar) */
3env(safe-area-inset-right)  /* Right edge (curved corners) */
4env(safe-area-inset-bottom) /* Bottom edge (home indicator) */
5env(safe-area-inset-left)   /* Left edge (curved corners) */

These values are dynamic and change based on:

  • Device orientation (portrait vs landscape)
  • Device hardware (notch position, corner radius)
  • Browser chrome visibility (address bar, toolbars)

Practical Examples

Fixed Header with Safe Area

A common pattern is a fixed header that accounts for the top safe area:

header.css
1.header {
2  position: fixed;
3  top: 0;
4  left: 0;
5  right: 0;
6
7  /* Add safe area to existing padding */
8  padding-top: calc(1rem + env(safe-area-inset-top));
9  padding-left: calc(1rem + env(safe-area-inset-left));
10  padding-right: calc(1rem + env(safe-area-inset-right));
11
12  background: white;
13}

Try It: Fixed Header

This sandbox simulates a device with a notch using CSS custom properties. Toggle the "notch" to see how env() adapts the layout:

/* Simulating env() safe area insets with CSS variables */
:root {
  --safe-area-inset-top: 44px;
  --safe-area-inset-bottom: 34px;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  margin: 0;
  padding: 20px;
  background: #1a1a2e;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.device-frame {
  width: 280px;
  height: 500px;
  background: #0f0f0f;
  border-radius: 40px;
  padding: 8px;
  position: relative;
  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}

.notch {
  position: absolute;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  width: 120px;
  height: 28px;
  background: #0f0f0f;
  border-radius: 0 0 20px 20px;
  z-index: 10;
  transition: opacity 0.3s;
}

.device-frame.no-notch .notch {
  opacity: 0;
}

.device-frame.no-notch {
  --safe-area-inset-top: 0px;
}

.screen {
  width: 100%;
  height: 100%;
  background: #f5f5f5;
  border-radius: 32px;
  overflow: hidden;
  position: relative;
}

.header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  background: #2563eb;
  color: white;
  display: flex;
  align-items: center;
  justify-content: space-between;

  /* Using safe area inset */
  padding-top: calc(12px + var(--safe-area-inset-top));
  padding-left: 16px;
  padding-right: 16px;
  padding-bottom: 12px;

  transition: padding-top 0.3s;
}

.header h1 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
}

.back-btn, .menu-btn {
  font-size: 20px;
  cursor: pointer;
}

.content {
  padding: 100px 16px 16px;
  color: #333;
  line-height: 1.6;
}

.content p {
  margin: 0 0 12px;
  font-size: 14px;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

.toggle input {
  width: 18px;
  height: 18px;
  cursor: pointer;
}

Bottom Navigation Bar

For bottom-fixed navigation, account for the home indicator:

nav.css
1.bottom-nav {
2  position: fixed;
3  bottom: 0;
4  left: 0;
5  right: 0;
6
7  /* Ensure nav clears the home indicator */
8  padding-bottom: env(safe-area-inset-bottom);
9  padding-left: env(safe-area-inset-left);
10  padding-right: env(safe-area-inset-right);
11
12  background: white;
13}

Try It: Bottom Navigation

See how the bottom navigation adapts when the home indicator is present:

/* Simulating env() safe area insets */
:root {
  --safe-area-inset-bottom: 34px;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  margin: 0;
  padding: 20px;
  background: #1a1a2e;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.device-frame {
  width: 280px;
  height: 500px;
  background: #0f0f0f;
  border-radius: 40px;
  padding: 8px;
  position: relative;
  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}

.device-frame.no-indicator {
  --safe-area-inset-bottom: 0px;
}

.device-frame.no-indicator .home-indicator {
  opacity: 0;
}

.screen {
  width: 100%;
  height: 100%;
  background: #f5f5f5;
  border-radius: 32px;
  overflow: hidden;
  position: relative;
}

.content {
  padding: 24px 16px;
  padding-bottom: 120px;
  color: #333;
}

.content h2 {
  margin: 0 0 16px;
  font-size: 20px;
  color: #1a1a1a;
}

.content p {
  margin: 0 0 12px;
  font-size: 14px;
  line-height: 1.6;
}

.bottom-nav {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: white;
  display: flex;
  justify-content: space-around;
  border-top: 1px solid #e5e5e5;

  /* Using safe area inset */
  padding-top: 8px;
  padding-bottom: calc(8px + var(--safe-area-inset-bottom));

  transition: padding-bottom 0.3s;
}

.nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  background: none;
  border: none;
  padding: 4px 12px;
  cursor: pointer;
  color: #666;
}

.nav-item.active {
  color: #2563eb;
}

.nav-item .icon {
  font-size: 20px;
}

.nav-item .label {
  font-size: 10px;
  font-weight: 500;
}

.home-indicator {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  width: 120px;
  height: 4px;
  background: #333;
  border-radius: 2px;
  transition: opacity 0.3s;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

.toggle input {
  width: 18px;
  height: 18px;
  cursor: pointer;
}

Full-Screen Hero Section

For immersive hero sections that extend edge-to-edge:

hero.css
1.hero {
2  min-height: 100vh;
3  min-height: 100dvh; /* Dynamic viewport height */
4
5  /* Content padding respects all safe areas */
6  padding:
7      calc(2rem + env(safe-area-inset-top))
8      calc(1.5rem + env(safe-area-inset-right))
9      calc(2rem + env(safe-area-inset-bottom))
10      calc(1.5rem + env(safe-area-inset-left));
11}

Try It: Complete App Layout

A full example combining header, content, and bottom navigation with all safe area insets:

/* Simulating env() with CSS custom properties */
:root {
  --safe-area-inset-top: 44px;
  --safe-area-inset-bottom: 34px;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  margin: 0;
  padding: 20px;
  background: #1a1a2e;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.device-frame {
  width: 280px;
  height: 520px;
  background: #0f0f0f;
  border-radius: 40px;
  padding: 8px;
  position: relative;
  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}

.device-frame.no-notch {
  --safe-area-inset-top: 0px;
}

.device-frame.no-notch .notch { opacity: 0; }

.device-frame.no-indicator {
  --safe-area-inset-bottom: 0px;
}

.device-frame.no-indicator .home-indicator { opacity: 0; }

.notch {
  position: absolute;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  width: 120px;
  height: 28px;
  background: #0f0f0f;
  border-radius: 0 0 20px 20px;
  z-index: 10;
  transition: opacity 0.3s;
}

.screen {
  width: 100%;
  height: 100%;
  background: #fff;
  border-radius: 32px;
  overflow: hidden;
  position: relative;
}

.header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(10px);
  display: flex;
  align-items: center;
  justify-content: space-between;
  z-index: 5;

  /* Safe area handling */
  padding-top: calc(8px + var(--safe-area-inset-top));
  padding-left: 16px;
  padding-right: 16px;
  padding-bottom: 8px;

  transition: padding-top 0.3s;
}

.header h1 {
  margin: 0;
  font-size: 28px;
  font-weight: 700;
}

.header-btn {
  background: none;
  border: none;
  color: #007aff;
  font-size: 16px;
  cursor: pointer;
}

.content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  overflow-y: auto;

  /* Account for header and nav */
  padding-top: calc(60px + var(--safe-area-inset-top));
  padding-bottom: calc(70px + var(--safe-area-inset-bottom));
  padding-left: 2px;
  padding-right: 2px;

  transition: padding 0.3s;
}

.photo-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px;
}

.photo {
  aspect-ratio: 1;
}

.bottom-nav {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(10px);
  display: flex;
  justify-content: space-around;
  border-top: 1px solid #e5e5e5;
  z-index: 5;

  /* Safe area handling */
  padding-top: 6px;
  padding-bottom: calc(6px + var(--safe-area-inset-bottom));

  transition: padding-bottom 0.3s;
}

.nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  background: none;
  border: none;
  padding: 2px 8px;
  cursor: pointer;
  color: #8e8e93;
  font-size: 10px;
}

.nav-item.active { color: #007aff; }
.nav-item .icon { font-size: 22px; }

.home-indicator {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  width: 120px;
  height: 4px;
  background: #000;
  border-radius: 2px;
  z-index: 10;
  transition: opacity 0.3s;
}

.controls {
  display: flex;
  gap: 20px;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  color: white;
  cursor: pointer;
  font-size: 13px;
}

.toggle input {
  width: 16px;
  height: 16px;
  cursor: pointer;
}

Fallback Values

The env() function accepts a fallback value for browsers that don't support safe area insets or on devices without hardware intrusions:

fallback.css
1.element {
2  /* Fallback to 0px if env() is not supported */
3  padding-top: env(safe-area-inset-top, 0px);
4
5  /* Fallback to a default value */
6  padding-bottom: env(safe-area-inset-bottom, 1rem);
7
8  /* Use in calculations */
9  margin-top: calc(20px + env(safe-area-inset-top, 0px));
10}
Note

On devices without notches or when viewport-fit is not set to cover, safe area inset values resolve to 0px.

Using with CSS Custom Properties

Combine env() with CSS custom properties for more maintainable code:

variables.css
1:root {
2  --safe-top: env(safe-area-inset-top, 0px);
3  --safe-right: env(safe-area-inset-right, 0px);
4  --safe-bottom: env(safe-area-inset-bottom, 0px);
5  --safe-left: env(safe-area-inset-left, 0px);
6
7  --header-height: calc(60px + var(--safe-top));
8  --nav-height: calc(56px + var(--safe-bottom));
9}
10
11.header {
12  height: var(--header-height);
13  padding-top: var(--safe-top);
14}
15
16.main {
17  padding-top: var(--header-height);
18  padding-bottom: var(--nav-height);
19}
20
21.bottom-nav {
22  height: var(--nav-height);
23  padding-bottom: var(--safe-bottom);
24}

Tailwind CSS Integration

If you're using Tailwind CSS, you can extend your config to include safe area utilities:

tailwind.config.js
1export default {
2  theme: {
3      extend: {
4          padding: {
5              'safe-top': 'env(safe-area-inset-top)',
6              'safe-right': 'env(safe-area-inset-right)',
7              'safe-bottom': 'env(safe-area-inset-bottom)',
8              'safe-left': 'env(safe-area-inset-left)',
9          },
10          margin: {
11              'safe-top': 'env(safe-area-inset-top)',
12              'safe-bottom': 'env(safe-area-inset-bottom)',
13          },
14      },
15  },
16};

Then use them in your markup:

component.html
1<header class="fixed top-0 inset-x-0 pt-safe-top px-safe-left">
2  <!-- Header content -->
3</header>
4
5<nav class="fixed bottom-0 inset-x-0 pb-safe-bottom">
6  <!-- Navigation content -->
7</nav>

Landscape Orientation

Safe area insets become particularly important in landscape mode, where notches appear on the left or right side:

landscape.css
1/* Handle landscape orientation */
2@media (orientation: landscape) {
3  .sidebar {
4      /* Account for notch on left side */
5      padding-left: calc(1rem + env(safe-area-inset-left));
6  }
7
8  .content {
9      /* Account for potential notch on right */
10      padding-right: calc(1rem + env(safe-area-inset-right));
11  }
12}

Try It: Landscape Orientation

In landscape mode, the notch shifts to the side. Toggle to see the difference:

/* Landscape safe area simulation */
:root {
  --safe-area-inset-left: 44px;
  --safe-area-inset-right: 0px;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  margin: 0;
  padding: 20px;
  background: #1a1a2e;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.device-frame.landscape {
  width: 480px;
  height: 280px;
  background: #0f0f0f;
  border-radius: 30px;
  padding: 8px;
  position: relative;
  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}

.device-frame.no-notch {
  --safe-area-inset-left: 0px;
}

.device-frame.no-notch .notch {
  opacity: 0;
}

.notch {
  position: absolute;
  left: 8px;
  top: 50%;
  transform: translateY(-50%);
  width: 28px;
  height: 100px;
  background: #0f0f0f;
  border-radius: 0 16px 16px 0;
  z-index: 10;
  transition: opacity 0.3s;
}

.screen {
  width: 100%;
  height: 100%;
  background: #1a1a1a;
  border-radius: 22px;
  overflow: hidden;
  display: flex;
}

.sidebar {
  background: #252525;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 12px;

  /* Safe area on left */
  padding-left: calc(12px + var(--safe-area-inset-left));

  transition: padding-left 0.3s;
}

.sidebar-item {
  color: #888;
  font-size: 13px;
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
}

.sidebar-item.active {
  background: #333;
  color: white;
}

.sidebar-item:hover:not(.active) {
  background: #2a2a2a;
}

.main-content {
  flex: 1;
  padding: 16px;
  display: flex;
  flex-direction: column;
}

.main-content h2 {
  margin: 0 0 12px;
  color: white;
  font-size: 18px;
  font-weight: 600;
}

.video-placeholder {
  flex: 1;
  background: #333;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.video-placeholder span {
  font-size: 40px;
  opacity: 0.5;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

.toggle input {
  width: 18px;
  height: 18px;
  cursor: pointer;
}

Browser Support

The env() function and safe area insets have excellent browser support:

BrowserSupport
Chrome69+
Firefox65+
Safari11.1+
Edge79+
iOS Safari11.2+
Chrome Android69+
Good News

All modern browsers support env() and safe area insets. The only consideration is providing fallback values for older browsers or non-notched devices.

Common Pitfalls

Forgetting viewport-fit

Safe area insets only work when viewport-fit=cover is set:

index.html
1<!-- This won't work - missing viewport-fit -->
2<meta name="viewport" content="width=device-width, initial-scale=1">
3
4<!-- This works -->
5<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

Using env() in Unsupported Contexts

The env() function works in property values but not in media queries:

example.css
1/* This works */
2.element {
3  padding-top: env(safe-area-inset-top);
4}
5
6/* This does NOT work */
7@media (min-height: env(safe-area-inset-top)) {
8  /* Media queries don't support env() */
9}

Doubling Up Safe Areas

When nesting elements, be careful not to apply safe area padding multiple times:

nesting.css
1/* Parent already has safe area padding */
2.app-shell {
3  padding-top: env(safe-area-inset-top);
4}
5
6/* Don't add it again to children */
7.header {
8  /* This would double the spacing */
9  /* padding-top: env(safe-area-inset-top); */
10
11  /* Just use regular padding */
12  padding-top: 1rem;
13}

Summary

The env() function with safe area insets is essential for modern mobile web development:

  1. Enable full-screen mode with viewport-fit=cover
  2. Use env() variables for top, right, bottom, and left insets
  3. Provide fallbacks for older browsers and standard displays
  4. Combine with calc() to add safe areas to existing spacing
  5. Test in landscape where notches shift position

By properly handling safe areas, your web apps will feel native and professional across all modern devices.

References

GET IN TOUCH

Let's work together

I build fast, accessible, and delightful digital experiences for the web.
Whether you have a project in mind or just want to connect, I'd love to hear from you.

or reach out directly at hello@mohammadshehadeh.com