Understanding env() Safe Area Insets in CSS
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)
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:
The viewport-fit property accepts three values:
| Value | Behavior |
|---|---|
auto | Default letterboxing, content stays in safe area |
contain | Same as auto, explicit safe area containment |
cover | Content extends to full screen, you handle safe areas |
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:
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:
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:
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:
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:
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:
Tailwind CSS Integration
If you're using Tailwind CSS, you can extend your config to include safe area utilities:
Then use them in your markup:
Landscape Orientation
Safe area insets become particularly important in landscape mode, where notches appear on the left or right side:
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:
| Browser | Support |
|---|---|
| Chrome | 69+ |
| Firefox | 65+ |
| Safari | 11.1+ |
| Edge | 79+ |
| iOS Safari | 11.2+ |
| Chrome Android | 69+ |
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:
Using env() in Unsupported Contexts
The env() function works in property values but not in media queries:
Doubling Up Safe Areas
When nesting elements, be careful not to apply safe area padding multiple times:
Summary
The env() function with safe area insets is essential for modern mobile web development:
- Enable full-screen mode with
viewport-fit=cover - Use
env()variables for top, right, bottom, and left insets - Provide fallbacks for older browsers and standard displays
- Combine with
calc()to add safe areas to existing spacing - Test in landscape where notches shift position
By properly handling safe areas, your web apps will feel native and professional across all modern devices.

