The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
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

01

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.

Edge-to-edge document with a protected content shell
<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>
Extend the surface while keeping content visible
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));
}
Back to quick reference ↑
02

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.

Combine parser, scalar, indexed, and nested fallbacks
.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)
  );
}
Back to quick reference ↑
03

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.

A responsive bottom bar and a stable reserved region
.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));
}
Back to quick reference ↑
04

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.

A chat layout that clears an overlaid keyboard
<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>
Move the composer and preserve its scroll clearance
.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));
}
Back to quick reference ↑
05

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.

Request the overlay mode with a standalone fallback
{
  "name": "Field Notes",
  "start_url": "/",
  "display": "standalone",
  "display_override": ["window-controls-overlay"]
}
Fit a custom bar inside the exposed title-bar rectangle
.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;
  }
}
Back to quick reference ↑
06

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.

Promote one column into two hinge-aware panes
.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; }
}
Address top and bottom segments with x-y indices
@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)
    );
  }
}
Back to quick reference ↑
07

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.

Protect physical edges and keep content flow-relative
.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;
}
Map physical edges to logical tokens for horizontal text
: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);
}
Back to quick reference ↑
08

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.

A zoomable viewport and layered dynamic-height enhancement
<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>
Keep the baseline useful before environment data arrives
.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;
}
Back to quick reference ↑

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.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. CSS Working GroupCSS Environment Variables Module Level 1drafts.csswg.org
  2. World Wide Web ConsortiumCSS Viewport Module Level 1w3.org
  3. World Wide Web ConsortiumMedia Queries Level 5w3.org
  4. Web Editing Working GroupVirtualKeyboard APIw3c.github.io
  5. Web Incubator Community GroupWindow Controls Overlaywicg.github.io
  6. World Wide Web ConsortiumCSS Logical Properties and Values Module Level 1w3.org
  7. Web Accessibility InitiativeMeta viewport allows for zoomw3.org
  8. 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.

Share feedback