The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Opt into edge-to-edge rendering | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | View examples |
| Protect all four viewport edges | .shell {
padding:
env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
} | View examples |
| Preserve design spacing | .shell {
padding-top: max(1rem, env(safe-area-inset-top, 0px));
} | View examples |
| Supply an undefined-variable fallback | .toolbar { bottom: env(keyboard-inset-height, 0px); } | View examples |
| Fallback after segment indices | .pane {
inline-size: env(viewport-segment-width 0 0, 100vw);
} | View examples |
| Chain environment fallbacks | .dock {
bottom:
env(keyboard-inset-height, env(safe-area-inset-bottom, 0px));
} | View examples |
| Keep a declaration-level fallback | .shell {
padding-bottom: 1rem;
padding-bottom:
max(1rem, env(safe-area-inset-bottom, 0px));
} | View examples |
| Follow the current safe edge | .bottom-bar {
padding-bottom:
max(0.75rem, env(safe-area-inset-bottom, 0px));
} | View examples |
| Reserve the maximum safe inset | .stable-panel {
padding-bottom: env(safe-area-max-inset-bottom, 0px);
} | View examples |
| Lift a fixed action from the edge | .fab {
bottom: max(1rem, env(safe-area-inset-bottom, 0px));
right: max(1rem, env(safe-area-inset-right, 0px));
} | View examples |
| Declare keyboard overlay behavior | <meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=overlays-content"> | View examples |
| Clear an overlaid keyboard | .composer { bottom: env(keyboard-inset-height, 0px); } | View examples |
| Preserve focused-field scroll space | .messages {
scroll-padding-bottom:
calc(5rem + env(keyboard-inset-height, 0px));
} | View examples |
| Fit an installed app title bar | .titlebar {
left: env(titlebar-area-x, 0px);
width: env(titlebar-area-width, 100%);
height: env(titlebar-area-height, 3rem);
} | View examples |
| Target window-controls-overlay mode | @media (display-mode: window-controls-overlay) {
.titlebar { display: flex; };
} | View examples |
| Detect side-by-side segments | @media (horizontal-viewport-segments: 2) and (vertical-viewport-segments: 1) {
.workspace { display: grid; };
} | View examples |
| Size both viewport panes | .workspace {
grid-template-columns:
env(viewport-segment-width 0 0) env(viewport-segment-width 1 0);
} | View examples |
| Measure the hinge gap | .workspace {
column-gap:
calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0));
} | View examples |
| Keep device insets physical | .shell {
padding-left: env(safe-area-inset-left, 0px);
padding-right: env(safe-area-inset-right, 0px);
} | View examples |
| Remap a logical start token for RTL | :root:dir(rtl) {
--safe-inline-start: env(safe-area-inset-right, 0px);
} | View examples |
| Keep browser zoom available | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | View examples |
| Track the dynamic viewport | .screen { min-height: 100vh; min-height: 100dvh; } | View examples |
CSS environment variables expose user-agent measurements that ordinary custom properties cannot know: safe viewport edges, virtual-keyboard geometry, desktop title-bar space, and the panes of a segmented display. This guide starts with resilient defaults, opts into edge-to-edge rendering deliberately, and treats newer variables as progressive enhancement so content remains usable when a browser, device, or installation mode does not provide them.
Step by step
Detailed examples
Opt into edge-to-edge space, then protect content
On supporting mobile browsers, viewport-fit=cover allows page backgrounds and decorative surfaces to extend across the full display. That opt-in does not make controls safe by itself. Apply the four safe-area-inset-* values to the element that owns essential content. Each value is a physical distance from a viewport edge and is zero on an ordinary rectangular viewport. max() combines a required inset with the design system's normal spacing instead of accidentally replacing that spacing with zero.
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<main class="safe-shell">
<h1>Trip planner</h1>
<button type="button">Save route</button>
</main> html {
background: #0f172a;
}
body {
margin: 0;
}
.safe-shell {
min-height: 100dvh;
padding-top: max(1rem, env(safe-area-inset-top, 0px));
padding-right: max(1rem, env(safe-area-inset-right, 0px));
padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px));
padding-left: max(1rem, env(safe-area-inset-left, 0px));
} Layer fallbacks at the correct level
env() accepts a case-sensitive environment-variable name, optional space-separated non-negative integer indices, and an optional fallback after a comma. Everything after that first comma belongs to the fallback, so the fallback may itself contain commas or another env(). It is used when the requested variable is undefined; it does not make an engine understand env() or another unsupported function. Keep a plain declaration first when older parsers matter. If an undefined variable has no fallback, the containing declaration becomes invalid at computed-value time, and an invalid substitution inside a shorthand invalidates the whole shorthand.
.panel {
padding-bottom: 1rem;
padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px));
inline-size: 100%;
inline-size: env(viewport-segment-width 0 0, 100%);
}
.composer {
bottom: env(
keyboard-inset-height,
env(safe-area-inset-bottom, 0px)
);
} Choose dynamic movement or stable reservation
safe-area-inset-* values describe the current safe rectangle and may change as dynamic user-agent interfaces appear or retract. The safe-area-max-inset-* counterparts are static maximum values associated with the state where dynamic interfaces are retracted and the layout viewport has its large size. Use current insets for controls that must follow the visible edge. Consider a maximum inset when avoiding layout movement is more important, but retain a fallback because the maximum variables are newer. Test browser chrome expanded and collapsed, portrait and landscape, and installed-app modes rather than assuming one device measurement.
.bottom-bar {
position: sticky;
bottom: 0;
padding: 0.75rem max(1rem, env(safe-area-inset-right, 0px));
padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
padding-left: max(1rem, env(safe-area-inset-left, 0px));
}
.stable-stage {
padding-bottom: 0;
padding-bottom: env(safe-area-max-inset-bottom, 0px);
}
.fab {
position: fixed;
right: max(1rem, env(safe-area-inset-right, 0px));
bottom: max(1rem, env(safe-area-inset-bottom, 0px));
} Treat virtual-keyboard geometry as progressive enhancement
The VirtualKeyboard specification defines keyboard-inset-top, -right, -bottom, -left, -width, and -height. They update from the keyboard bounding rectangle and otherwise resolve to zero in supporting implementations. interactive-widget=overlays-content requests a keyboard that does not resize either viewport; this makes explicit avoidance necessary and is useful only when the application owns that behavior. Support remains limited, so a normal document-flow layout must work first. Add scroll padding so focused controls are not hidden, and never rely only on a fixed footer offset to keep form fields perceivable.
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=overlays-content">
<main class="chat">
<ol class="messages"><li>Ready when you are.</li></ol>
<form class="composer">
<label for="reply">Reply</label>
<input id="reply" name="reply">
<button>Send</button>
</form>
</main> .chat {
min-height: 100dvh;
padding-bottom: calc(4.5rem + env(keyboard-inset-height, 0px));
}
.messages {
overflow: auto;
scroll-padding-bottom: calc(5rem + env(keyboard-inset-height, 0px));
}
.composer {
position: fixed;
right: 0;
bottom: env(keyboard-inset-height, 0px);
left: 0;
padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
} Use title-bar space only when an installed app exposes it
The Window Controls Overlay draft defines titlebar-area-x, -y, -width, and -height for installed desktop web apps running in window-controls-overlay mode. The rectangle is the title-bar region available to web content; it excludes native minimize, maximize, and close controls. Its coordinates are physical, can change after window resize or zoom, and are not a general-browser safe area. Provide both manifest and CSS fallbacks, gate the enhanced presentation with the display-mode media feature, and keep draggable regions separate from interactive buttons.
{
"name": "Field Notes",
"start_url": "/",
"display": "standalone",
"display_override": ["window-controls-overlay"]
} .titlebar {
display: none;
}
@media (display-mode: window-controls-overlay) {
.titlebar {
position: fixed;
top: env(titlebar-area-y, 0px);
left: env(titlebar-area-x, 0px);
display: flex;
width: env(titlebar-area-width, 100%);
height: env(titlebar-area-height, 3rem);
align-items: center;
app-region: drag;
}
.titlebar button,
.titlebar a {
app-region: no-drag;
}
} Detect segments before reading their indexed geometry
A hinge or fold can divide the viewport into logical segments. Media Queries Level 5 reports the count in each physical direction, while the six viewport-segment-* environment variables report a segment's physical dimensions and edges. Their two indices are x then y: 0 0 is the leftmost top segment, 1 0 is the next segment to its right, and 0 1 is the next row. The variables are defined only when the viewport has multiple segments, so start with a single-pane layout and read indexed values only inside a matching segment query. The separator can occupy space; compute it from adjacent edge coordinates instead of guessing a hinge width.
.workspace {
display: grid;
grid-template-columns: minmax(0, 1fr);
}
@media (horizontal-viewport-segments: 2) and (vertical-viewport-segments: 1) {
.workspace {
grid-template-columns:
env(viewport-segment-width 0 0)
env(viewport-segment-width 1 0);
column-gap: calc(
env(viewport-segment-left 1 0) -
env(viewport-segment-right 0 0)
);
}
.navigation { grid-column: 1; }
.document { grid-column: 2; }
} @media (horizontal-viewport-segments: 1) and (vertical-viewport-segments: 2) {
.workspace {
grid-template-rows:
env(viewport-segment-height 0 0)
env(viewport-segment-height 0 1);
row-gap: calc(
env(viewport-segment-top 0 1) -
env(viewport-segment-bottom 0 0)
);
}
} Do not rotate physical insets with the writing mode
Environment-variable suffixes such as left and top name physical viewport edges. direction and writing-mode do not change those measurements. For a full-page safe shell, pair them with physical padding-left, padding-right, padding-top, and padding-bottom; putting safe-area-inset-left into padding-inline-start would move the left measurement to the right side in horizontal RTL text. Inside that protected shell, continue using logical CSS for content flow. If a horizontal component needs named logical safe-edge tokens, remap both tokens under :dir(rtl); vertical writing modes need their own mapping because inline start may be the physical top or bottom.
.safe-shell {
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);
}
.card {
border-inline-start: 0.25rem solid #2563eb;
margin-inline: auto;
max-inline-size: 42rem;
} :root {
--safe-inline-start: env(safe-area-inset-left, 0px);
--safe-inline-end: env(safe-area-inset-right, 0px);
}
:root:dir(rtl) {
--safe-inline-start: env(safe-area-inset-right, 0px);
--safe-inline-end: env(safe-area-inset-left, 0px);
}
.horizontal-toolbar {
padding-inline: var(--safe-inline-start) var(--safe-inline-end);
} Test the layout, not just feature parsing
env() itself is broadly available, but individual environment variables and segment media features have different support and activation conditions. A fallback proves that a declaration computes; it cannot prove that a device reports meaningful geometry. Likewise, @supports can detect env() syntax but not whether a particular environment variable is currently defined or nonzero. Preserve a one-column, document-flow baseline, then test notches, orientation changes, expanded browser chrome, an open keyboard, installed desktop mode, folds, RTL, vertical writing, text enlargement, and browser zoom. Do not disable zoom with user-scalable=no or a restrictive maximum-scale. Safe insets prevent physical occlusion; responsive reflow and visible focus still require ordinary accessible layout work.
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<main class="screen">
<h1>Account</h1>
<a href="#settings">Skip to settings</a>
</main> .screen {
box-sizing: border-box;
min-height: 100vh;
min-height: 100dvh;
padding: 1rem;
padding-top: max(1rem, env(safe-area-inset-top, 0px));
padding-right: max(1rem, env(safe-area-inset-right, 0px));
padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px));
padding-left: max(1rem, env(safe-area-inset-left, 0px));
}
:focus-visible {
outline: 0.2rem solid #f59e0b;
outline-offset: 0.2rem;
} Local code tester
Build a safe, direction-aware application shell
Resize the preview and compare LTR and RTL cards. On a device that reports safe-area insets, the outer shell keeps every interactive control inside the visible region.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- CSS Working GroupCSS Environment Variables Module Level 1drafts.csswg.org
- World Wide Web ConsortiumCSS Viewport Module Level 1w3.org
- World Wide Web ConsortiumMedia Queries Level 5w3.org
- Web Editing Working GroupVirtualKeyboard APIw3c.github.io
- Web Incubator Community GroupWindow Controls Overlaywicg.github.io
- World Wide Web ConsortiumCSS Logical Properties and Values Module Level 1w3.org
- Web Accessibility InitiativeMeta viewport allows for zoomw3.org
- Apple DeveloperDesign for Safari 15developer.apple.com
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



