mcpField
S
safe-area-handling

safe-area-handling

github.com/cap-goDesign & UI731.3k installsUpdated 2w ago
Repository

About safe-area-handling

safe-area-handling is a development skill for diagnosing and handling system safe areas in Capacitor projects across iOS, Android, and windowed layouts. It covers version differences, CSS insets, status bar migration, and device verification.

Key features

  1. 01Inspect viewport and Capacitor config
  2. 02Handle iOS and Android insets
  3. 03Adapt landscape and windowed layouts
  4. 04Migrate Android status bar handling
  5. 05Verify on devices and troubleshoot

Use cases

  1. 01Fix content hidden by notches or system bars
  2. 02Diagnose zero or incorrect safe-area values
  3. 03Address layout issues after Capacitor upgrades

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/cap-go/capgo-skills --skill safe-area-handling

After installing, type/safe-area-handlingin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

metadata.json
references/
android-edge-to-edge.md
css-patterns.md

Details

From the project README

Safe Area Handling in Capacitor

When to Use

TRIGGER when:

  • Content renders under the status bar, notch, Dynamic Island, cutout, home indicator, or Android navigation/gesture bar
  • env(safe-area-inset-*) is 0px on Android, or correct only after rotation
  • Layout broke after Capacitor 8 upgrade or raising targetSdkVersion to 35/36
  • StatusBar.setOverlaysWebView / setBackgroundColor stopped working on Android
  • Landscape / split-screen / iPad windowed layouts have wrong side padding

Do not use:

  • Keyboard covering inputs: capacitor-keyboard
  • Ionic ion-header/ion-toolbar theming: ionic-design; Konsta safeAreas: konsta-ui
  • Tailwind plugin/utility setup: tailwind-capacitor
  • iOS scene sizing, UIScreen.main, multi-window: capacitor-ios-resizability
  • Splash screen colors: capacitor-splash-screen

Facts that drive every decision

Platform / versionBehavior
iOS (all)WebView is full-screen. env(safe-area-inset-*) works only with viewport-fit=cover. ios.contentInset defaults to never.
Android, Capacitor <= 7android.adjustMarginsForEdgeToEdge config could add native margins.
Android, Capacitor 8+adjustMarginsForEdgeToEdge removed. SystemBars core plugin (in @capacitor/core) handles insets.
Android 15 (targetSdk 35)Edge-to-edge enforced; opt-out via windowOptOutEdgeToEdgeEnforcement still possible.
Android 16 (targetSdk 36, Capacitor 8 default; 37 on Capacitor 9)Opt-out ignored. @capacitor/status-bar overlaysWebView and backgroundColor have no effect.
Android WebView < 140env(safe-area-inset-*) wrong/0 (Chromium bug). SystemBars injects --safe-area-inset-* variables (default insetsHandling: 'css').

How SystemBars behaves on Android (from core source): if the page has viewport-fit=cover and WebView >= 140, insets pass through to env() and the WebView is edge-to-edge. Otherwise Capacitor pads the WebView natively so content sits between the bars and env() reports 0. --safe-area-inset-* variables carry correct values in both cases when insetsHandling is css.

Workflow

  1. Inspect: index.html viewport meta, capacitor.config.* (ios.contentInset, android.adjustMarginsForEdgeToEdge, plugins.SystemBars, plugins.StatusBar), @capacitor/core + @capacitor/status-bar versions, android/variables.gradle targetSdkVersion, global CSS that sets padding-top, UI framework (Ionic / Konsta handle insets themselves).
  2. Decide the design: full-bleed (draw under bars, pad content) vs. contained (content between bars). Ask the user if mockups are unclear.
  3. Full-bleed (recommended): viewport-fit=cover + the CSS fallback chain below, applied once at the layout shell, not per component.
  4. Remove legacy config: delete android.adjustMarginsForEdgeToEdge; drop Android uses of StatusBar.setOverlaysWebView / setBackgroundColor (keep for iOS if wanted); prefer SystemBars.setStyle for icon color.
  5. Framework check: Ionic already applies --ion-safe-area-*; Konsta uses safeAreas. Do not double-pad.
  6. Verify on devices (below).

Only load a reference when its topic is in play:

Baseline

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
:root {
  --sat: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
  --sar: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
  --sab: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
  --sal: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
}
.app-header { padding-top: var(--sat); padding-inline: var(--sal) var(--sar); }
.app-tabbar { padding-bottom: var(--sab); padding-inline: var(--sal) var(--sar); }

The --safe-area-inset-* names are injected by Capacitor's SystemBars on Android; on iOS and web they are undefined, so the env() fallback applies.

import { SystemBars, SystemBarsStyle } from '@capacitor/core';
await SystemBars.setStyle({ style: SystemBarsStyle.Dark }); // light icons on dark background

SystemBarsStyle.Dark = light content for dark backgrounds; Light = dark content. iOS needs UIViewControllerBasedStatusBarAppearance = YES (template default).

Traps

  • Missing viewport-fit=cover is the #1 cause of env() = 0 on iOS and of Capacitor padding the WebView on Android.
  • Do not combine ios.contentInset: 'always' with CSS safe-area padding: insets apply twice.
  • Do not hardcode 20/44/47/59 px status bar heights; heights differ per device, orientation, and window size.
  • Insets change at runtime (rotation, iPad windowed mode, foldables, Android split screen, keyboard). Use CSS, or re-read on resize; never cache once at startup.
  • On iOS the bottom inset stays while the keyboard is visible; use max() with keyboard height (see capacitor-keyboard).
  • 100vh includes the area under system bars; use 100dvh or flex layout with a fixed shell.
  • Ionic: ion-content with fullscreen + ion-header already pads; adding body padding shifts everything twice.

Verification

  1. npx cap sync, run on: iPhone with Dynamic Island, iPhone SE-class (home button, small top inset), iPad (rotate + Stage Manager/windowed), Android 15/16 device or emulator in gesture and 3-button navigation, landscape.
  2. Check Android WebView version: adb shell dumpsys package com.google.android.webview | grep versionName (< 140 relies on injected variables).
  3. In Safari Web Inspector / chrome://inspect, run:
getComputedStyle(document.documentElement).getPropertyValue('--sat');
document.querySelector('meta[name=viewport]').content.includes('viewport-fit=cover');
  1. Confirm no element overlaps bars; tab bar taps work above the gesture bar; no double padding.
  2. Grep for leftovers:
grep -rn "adjustMarginsForEdgeToEdge\|setOverlaysWebView\|setBackgroundColor\|windowOptOutEdgeToEdgeEnforcement" capacitor.config.* src android/app/src/main/res 2>/dev/null

Error Handling

Symptom / messageCauseFix
Header under status bar on iOS, env() = 0No viewport-fit=coverAdd to viewport meta
Android: content correct but no edge-to-edge, env() = 0No viewport-fit=cover or WebView < 140, so Capacitor pads nativelyAdd viewport-fit=cover; use --safe-area-inset-* fallback
Android: double gap at topNative padding + your CSS padding with insetsHandling: 'disable' or custom MainActivity insets codeRemove custom setDecorFitsSystemWindows / inset listeners; keep SystemBars default
StatusBar.setBackgroundColor / setOverlaysWebView no effect on AndroidtargetSdk 35+/36 edge-to-edgeDraw a background under the bar in CSS; use SystemBars.setStyle for icon color
android.adjustMarginsForEdgeToEdge still in config after Capacitor 8 upgrade, margins goneOption removed and ignoredDelete it; rely on SystemBars + CSS
Logcat Unknown insetsHandling value '...'. Falling back to 'css'.Invalid SystemBars.insetsHandlingUse a value listed in the current docs (css, disable; newer cores also accept native)
Status bar text invisible (white on white)Style mismatchSystemBars.setStyle({ style: SystemBarsStyle.Light })
Layout jumps on first paint (Android)SystemBars detects viewport-fit after page commitSet plugins.SystemBars.initialViewportFitValueHint: 'cover' (in core config declarations; verify in your version)

Resources

Comments

Markdown supported