Skip to main content
safe-area-handling Complete guide to handling safe areas in Capacitor apps for iPhone notch, Dynamic Island, home indicator, and Android cutouts. Covers CSS, JavaScript, and native solutions. Use this skill when users have layout issues on modern devices.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/Cap-go/capgo-skills --skill safe-area-handling명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... name safe-area-handling description Complete guide to handling safe areas in Capacitor apps for iPhone notch, Dynamic Island, home indicator, and Android cutouts. Covers CSS, JavaScript, and native solutions. Use this skill when users have layout issues on modern devices.
Safe Area Handling in Capacitor
Handle iPhone notch, Dynamic Island, home indicator, and Android cutouts properly.
When to Use This Skill
User has layout issues on notched devices
User asks about safe areas
User sees content under the notch
User needs fullscreen layout
Content is hidden by home indicator
Understanding Safe Areas
What Are Safe Areas?
Safe areas are the regions of the screen not obscured by:
iPhone : Notch, Dynamic Island, home indicator, rounded corners
Android : Camera cutouts, navigation gestures, display cutouts
Safe Area Insets
Inset Description safe-area-inset-topNotch/Dynamic Island/status bar safe-area-inset-bottomHome indicator/navigation bar safe-area-inset-leftLeft edge (landscape) safe-area-inset-rightRight edge (landscape)
CSS Solution
Enable Viewport Coverage
<meta
name ="viewport"
=
/>
content
"width=device-width, initial-scale=1.0, viewport-fit=cover"
Important : viewport-fit=cover is required to access safe area insets.
Using CSS Environment Variables
.header {
padding-top : env (safe-area-inset-top);
}
.footer {
padding-bottom : env (safe-area-inset-bottom);
}
.header {
padding-top : env (safe-area-inset-top, 20px );
}
.content {
padding-top : calc (env (safe-area-inset-top) + 16px );
padding-bottom : calc (env (safe-area-inset-bottom) + 16px );
}
Full Page Layout
.app {
position : fixed;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
display : flex;
flex-direction : column;
}
.header {
padding-top : env (safe-area-inset-top);
padding-left : env (safe-area-inset-left);
padding-right : env (safe-area-inset-right);
background : #fff ;
}
.content {
flex : 1 ;
overflow-y : auto;
-webkit-overflow -scrolling: touch;
padding-left : env (safe-area-inset-left);
padding-right : env (safe-area-inset-right);
}
.footer {
padding-bottom : env (safe-area-inset-bottom);
padding-left : env (safe-area-inset-left);
padding-right : env (safe-area-inset-right);
background : #fff ;
}
Tab Bar with Safe Area .tab-bar {
position : fixed;
bottom : 0 ;
left : 0 ;
right : 0 ;
display : flex;
background : #fff ;
border-top : 1px solid #eee ;
padding-bottom : env (safe-area-inset-bottom);
}
.tab-bar-item {
flex : 1 ;
display : flex;
flex-direction : column;
align-items : center;
padding : 8px 0 ;
min-height : 49px ;
}
Full-Bleed Background with Safe Content .hero {
background : linear-gradient (to bottom, #4f46e5 , #7c3aed );
padding-top : calc (env (safe-area-inset-top) + 20px );
padding-left : env (safe-area-inset-left);
padding-right : env (safe-area-inset-right);
}
.hero-content {
max-width : 100% ;
}
JavaScript Solution
Reading Safe Area Values function getSafeAreaInsets ( ) {
const computedStyle = getComputedStyle (document .documentElement );
return {
top : parseInt (computedStyle.getPropertyValue ('--sat' ) || '0' ),
bottom : parseInt (computedStyle.getPropertyValue ('--sab' ) || '0' ),
left : parseInt (computedStyle.getPropertyValue ('--sal' ) || '0' ),
right : parseInt (computedStyle.getPropertyValue ('--sar' ) || '0' ),
};
}
function setSafeAreaProperties ( ) {
const style = document .documentElement .style ;
const temp = document .createElement ('div' );
temp.style .paddingTop = 'env(safe-area-inset-top)' ;
temp.style .paddingBottom = 'env(safe-area-inset-bottom)' ;
temp.style .paddingLeft = 'env(safe-area-inset-left)' ;
temp.style .paddingRight = 'env(safe-area-inset-right)' ;
document .body .appendChild (temp);
const computed = getComputedStyle (temp);
style.setProperty ('--sat' , computed.paddingTop );
style.setProperty ('--sab' , computed.paddingBottom );
style.setProperty ('--sal' , computed.paddingLeft );
style.setProperty ('--sar' , computed.paddingRight );
document .body .removeChild (temp);
}
window .addEventListener ('orientationchange' , () => {
setTimeout (setSafeAreaProperties, 100 );
});
React Hook import { useState, useEffect } from 'react' ;
interface SafeAreaInsets {
top : number ;
bottom : number ;
left : number ;
right : number ;
}
function useSafeArea ( ): SafeAreaInsets {
const [insets, setInsets] = useState<SafeAreaInsets >({
top : 0 ,
bottom : 0 ,
left : 0 ,
right : 0 ,
});
useEffect (() => {
function updateInsets ( ) {
const temp = document .createElement ('div' );
temp.style .cssText = `
position: fixed;
top: 0;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
` ;
document .body .appendChild (temp);
const computed = getComputedStyle (temp);
setInsets ({
top : parseFloat (computed.paddingTop ) || 0 ,
bottom : parseFloat (computed.paddingBottom ) || 0 ,
left : parseFloat (computed.paddingLeft ) || 0 ,
right : parseFloat (computed.paddingRight ) || 0 ,
});
document .body .removeChild (temp);
}
const handleOrientationChange = ( ) => {
setTimeout (updateInsets, 100 );
};
updateInsets ();
window .addEventListener ('resize' , updateInsets);
window .addEventListener ('orientationchange' , handleOrientationChange);
return () => {
window .removeEventListener ('resize' , updateInsets);
window .removeEventListener ('orientationchange' , handleOrientationChange);
};
}, []);
return insets;
}
function Header ( ) {
const { top } = useSafeArea ();
return (
<header style ={{ paddingTop: top }}>
App Header
</header >
);
}
Vue Composable import { ref, onMounted, onUnmounted } from 'vue' ;
export function useSafeArea ( ) {
const insets = ref ({
top : 0 ,
bottom : 0 ,
left : 0 ,
right : 0 ,
});
function updateInsets ( ) {
const temp = document .createElement ('div' );
temp.style .cssText = `
position: fixed;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
` ;
document .body .appendChild (temp);
const computed = getComputedStyle (temp);
insets.value = {
top : parseFloat (computed.paddingTop ) || 0 ,
bottom : parseFloat (computed.paddingBottom ) || 0 ,
left : parseFloat (computed.paddingLeft ) || 0 ,
right : parseFloat (computed.paddingRight ) || 0 ,
};
document .body .removeChild (temp);
}
onMounted (() => {
updateInsets ();
window .addEventListener ('resize' , updateInsets);
});
onUnmounted (() => {
window .removeEventListener ('resize' , updateInsets);
});
return insets;
}
Native iOS Configuration
Status Bar Style
import type { CapacitorConfig } from '@capacitor/cli' ;
const config : CapacitorConfig = {
ios : {
contentInset : 'automatic' ,
},
};
Extend Behind Safe Areas
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate : UIResponder , UIApplicationDelegate {
func application (
_ application : UIApplication ,
didFinishLaunchingWithOptions launchOptions : [UIApplication .LaunchOptionsKey : Any ]?
) -> Bool {
if let window = UIApplication .shared.windows.first {
window.backgroundColor = .clear
}
return true
}
}
Info.plist Settings
<key > UIViewControllerBasedStatusBarAppearance</key >
<true />
<key > UISupportedInterfaceOrientations</key >
<array >
<string > UIInterfaceOrientationPortrait</string >
<string > UIInterfaceOrientationLandscapeLeft</string >
<string > UIInterfaceOrientationLandscapeRight</string >
</array >
Native Android Configuration
Display Cutout Mode
<resources >
<style name ="AppTheme" parent ="Theme.AppCompat.NoActionBar" >
<item name ="android:windowLayoutInDisplayCutoutMode" > shortEdges</item >
</style >
</resources >
Edge-to-Edge Display
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
class MainActivity : BridgeActivity () {
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false )
} else {
@Suppress("DEPRECATION" )
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
}
}
}
AndroidManifest Configuration
<activity
android:name =".MainActivity"
android:theme ="@style/AppTheme"
android:windowSoftInputMode ="adjustResize"
android:configChanges ="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" >
</activity >
Capacitor Status Bar Plugin
Installation npm install @capacitor/status-bar
npx cap sync
Usage import { StatusBar , Style } from '@capacitor/status-bar' ;
await StatusBar .setStyle ({ style : Style .Dark });
await StatusBar .setBackgroundColor ({ color : '#ffffff' });
await StatusBar .hide ();
await StatusBar .show ();
await StatusBar .setOverlaysWebView ({ overlay : true });
Common Issues and Solutions
Issue: Content Behind Notch Solution : Add viewport-fit and safe area padding
<meta name ="viewport" content ="viewport-fit=cover" >
body {
padding-top : env (safe-area-inset-top);
}
Issue: Tab Bar Under Home Indicator Solution : Add bottom safe area padding
.tab-bar {
padding-bottom : env (safe-area-inset-bottom);
}
Issue: Landscape Layout Broken Solution : Handle left/right insets
.content {
padding-left : env (safe-area-inset-left);
padding-right : env (safe-area-inset-right);
}
Issue: Keyboard Pushes Content Solution : Use adjustResize and handle insets dynamically
import { Keyboard } from '@capacitor/keyboard' ;
Keyboard .addListener ('keyboardWillShow' , (info ) => {
document .body .style .paddingBottom = `${info.keyboardHeight} px` ;
});
Keyboard .addListener ('keyboardWillHide' , () => {
document .body .style .paddingBottom = 'env(safe-area-inset-bottom)' ;
});
Issue: Safe Areas Not Working in WebView Cause : Missing viewport-fit=cover
<meta
name ="viewport"
content ="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
Testing Safe Areas
iOS Simulator
Use iPhone with notch (iPhone 14 Pro, etc.)
Test both portrait and landscape
Test with keyboard visible
Android Emulator
Create emulator with camera cutout
Test navigation gesture mode
Test 3-button navigation mode
Preview Different Devices
.debug-safe-areas ::before {
content : '' ;
position : fixed;
top : 0 ;
left : 0 ;
right : 0 ;
height : env (safe-area-inset-top);
background : rgba (255 , 0 , 0 , 0.3 );
z-index : 9999 ;
pointer-events : none;
}
.debug-safe-areas ::after {
content : '' ;
position : fixed;
bottom : 0 ;
left : 0 ;
right : 0 ;
height : env (safe-area-inset-bottom);
background : rgba (0 , 0 , 255 , 0.3 );
z-index : 9999 ;
pointer-events : none;
}
Resources