safe-area-handling について
safe-area-handlingは、iOS、Android、ウィンドウ表示のCapacitorプロジェクトでシステムのセーフエリアを診断・処理する開発スキルです。バージョン差、CSSのインセット、ステータスバー移行、端末での検証を扱います。
主な機能
- 01ビューポートとCapacitor設定の確認
- 02iOSとAndroidのインセット処理
- 03横向きとウィンドウ表示への対応
- 04Androidステータスバー処理の移行
- 05端末での検証とトラブルシューティング
活用シーン
- 01ノッチやシステムバーに隠れる表示の修正
- 02セーフエリア値がゼロまたは不正な原因調査
- 03Capacitor更新後のレイアウト問題への対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cap-go/ capgo-skills --skill safe-area-handlingインストール後、Claude Code で/safe-area-handlingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.jsonreferences/android-edge-to-edge.mdcss-patterns.md詳細
Capacitor でのセーフエリア処理
使用するタイミング
次の場合に使用します:
- コンテンツがステータスバー、ノッチ、Dynamic Island、カットアウト、ホームインジケーター、Android のナビゲーションバー/ジェスチャーバーの下に表示される
- Android で
env(safe-area-inset-*)が0pxになる、または回転後にしか正しくならない - Capacitor 8 へのアップグレード、または
targetSdkVersionを 35/36 に引き上げた後にレイアウトが崩れた - Android で
StatusBar.setOverlaysWebView/setBackgroundColorが動作しなくなった - 横向き/分割画面/iPad のウィンドウ表示レイアウトで左右のパディングが正しくない
使用しないケース:
- キーボードが入力欄にかぶさる場合:
capacitor-keyboard - Ionic の
ion-header/ion-toolbarのテーマ設定:ionic-design、Konsta のsafeAreas:konsta-ui - Tailwind のプラグイン/ユーティリティの設定:
tailwind-capacitor - iOS のシーンサイズ、
UIScreen.main、マルチウィンドウ:capacitor-ios-resizability - スプラッシュ画面の色:
capacitor-splash-screen
すべての判断の基準となる事実
| プラットフォーム/バージョン | 動作 |
|---|---|
| iOS(すべて) | WebView は全画面表示です。env(safe-area-inset-*) が機能するのは viewport-fit=cover の場合のみです。ios.contentInset のデフォルトは never です。 |
| Android、Capacitor <= 7 | android.adjustMarginsForEdgeToEdge 設定でネイティブのマージンを追加できます。 |
| Android、Capacitor 8 以降 | adjustMarginsForEdgeToEdge は削除されました。SystemBars コアプラグイン(@capacitor/core 内)がインセットを処理します。 |
| Android 15(targetSdk 35) | エッジツーエッジが強制されます。windowOptOutEdgeToEdgeEnforcement によるオプトアウトは引き続き可能です。 |
| Android 16(targetSdk 36、Capacitor 8 のデフォルト。Capacitor 9 では 37) | オプトアウトは無視されます。@capacitor/status-bar の overlaysWebView と backgroundColor は効果がありません。 |
| Android WebView < 140 | env(safe-area-inset-*) の値が誤っている、または 0 になります(Chromium のバグ)。SystemBars は --safe-area-inset-* 変数を挿入します(デフォルトの insetsHandling: 'css')。 |
Android での SystemBars の動作(コアソースより):ページに viewport-fit=cover があり、かつ WebView がバージョン 140 以上の場合、インセットは env() に渡され、WebView はエッジツーエッジになります。それ以外の場合、Capacitor は WebView にネイティブのパディングを追加するため、コンテンツはバーの間に収まり、env() は 0 を返します。insetsHandling が css の場合、どちらのケースでも --safe-area-inset-* 変数に正しい値が設定されます。
ワークフロー
- 確認:
index.htmlの viewport meta、capacitor.config.*(ios.contentInset、android.adjustMarginsForEdgeToEdge、plugins.SystemBars、plugins.StatusBar)、@capacitor/coreと@capacitor/status-barのバージョン、android/variables.gradleのtargetSdkVersion、padding-topを設定するグローバル CSS、UI フレームワーク(Ionic/Konsta はそれぞれ独自にインセットを処理します)。 - デザインを決定:フルブリード(バーの下まで描画し、コンテンツにパディングを設定)にするか、コンテインド(コンテンツをバーの間に配置)にするかを決めます。モックアップが不明瞭な場合はユーザーに確認します。
- フルブリード(推奨):
viewport-fit=coverと以下の CSS フォールバックチェーンを、コンポーネントごとではなくレイアウトシェルに一度だけ適用します。 - 古い設定を削除:
android.adjustMarginsForEdgeToEdgeを削除します。Android でのStatusBar.setOverlaysWebView/setBackgroundColorの使用をやめます(必要なら iOS では維持します)。アイコンの色にはSystemBars.setStyleを優先して使います。 - フレームワークを確認:Ionic はすでに
--ion-safe-area-*を適用します。Konsta はsafeAreasを使用します。パディングを二重に適用しないでください。 - デバイスで検証します(下記参照)。
関連するトピックがある場合にのみ、該当するリファレンスを読み込みます:
- references/android-edge-to-edge.md - SystemBars の設定、ステータスバーの移行、ナビゲーションバーの色、WebView バージョンの確認
- references/css-patterns.md - レイアウトシェル、タブバー、JS でのインセットの取得、デバッグオーバーレイ、サイズ変更可能なウィンドウ
基本設定
<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); }
--safe-area-inset-* という名前の変数は、Android では Capacitor の SystemBars によって挿入されます。iOS と Web では未定義のため、env() のフォールバックが適用されます。
import { SystemBars, SystemBarsStyle } from '@capacitor/core';
await SystemBars.setStyle({ style: SystemBarsStyle.Dark }); // light icons on dark background
SystemBarsStyle.Dark は暗い背景向けの明るいコンテンツ、Light は暗いコンテンツを意味します。iOS では UIViewControllerBasedStatusBarAppearance = YES が必要です(テンプレートのデフォルト)。
注意点
viewport-fit=coverがないことは、iOS でenv()が 0 になる原因の第 1 位であり、Android で Capacitor が WebView にパディングを追加する原因にもなります。ios.contentInset: 'always'と CSS のセーフエリアパディングを併用しないでください。インセットが二重に適用されます。- ステータスバーの高さを 20/44/47/59 px に固定しないでください。高さはデバイス、向き、ウィンドウサイズによって異なります。
- インセットは実行中に変化します(回転、iPad のウィンドウ表示モード、折りたたみ式デバイス、Android の画面分割、キーボード)。CSS を使用するか、
resize時に再読み込みしてください。起動時に一度だけ取得してキャッシュしないでください。 - iOS ではキーボード表示中も下部のインセットが残ります。キーボードの高さと
max()を使用してください(capacitor-keyboardを参照)。 100vhにはシステムバーの下の領域も含まれます。100dvhを使用するか、固定シェルを使ったフレックスレイアウトにしてください。- Ionic:
ion-contentとfullscreen、ion-headerの組み合わせでは、すでにパディングが追加されています。body にパディングを追加すると、すべてが二重にずれます。
検証
npx cap syncを実行し、次の環境でテストします。Dynamic Island 搭載の iPhone、iPhone SE クラス(ホームボタン、小さな上部インセット)、iPad(回転+Stage Manager/ウィンドウ表示)、ジェスチャーナビゲーションと 3 ボタンナビゲーションの両方を使用する Android 15/16 のデバイスまたはエミュレーター、横向き表示。- Android WebView のバージョンを確認します:
adb shell dumpsys package com.google.android.webview | grep versionName(140 未満では注入された変数に依存します)。 - Safari Web インスペクターまたは
chrome://inspectで、次を実行します:
getComputedStyle(document.documentElement).getPropertyValue('--sat');
document.querySelector('meta[name=viewport]').content.includes('viewport-fit=cover');
- 要素がバーに重なっていないこと、ジェスチャーバーの上でタブバーをタップできること、パディングが二重になっていないことを確認します。
- 残存箇所を grep で検索します:
grep -rn "adjustMarginsForEdgeToEdge\|setOverlaysWebView\|setBackgroundColor\|windowOptOutEdgeToEdgeEnforcement" capacitor.config.* src android/app/src/main/res 2>/dev/null
エラー処理
| 症状/メッセージ | 原因 | 対処法 |
|---|---|---|
iOS でヘッダーがステータスバーの下に入り込む、env() = 0 | viewport-fit=cover がない | viewport meta に追加する |
Android:コンテンツは正しいがエッジツーエッジにならず、env() = 0 | viewport-fit=cover がない、または WebView < 140 のため、Capacitor がネイティブでパディングを追加している | viewport-fit=cover を追加する。--safe-area-inset-* のフォールバックを使用する |
| Android:上部に二重の隙間がある | insetsHandling: 'disable' またはカスタム MainActivity のインセットコードによるネイティブパディングと CSS パディングの併用 | カスタムの setDecorFitsSystemWindows/インセットリスナーを削除し、SystemBars のデフォルト設定を維持する |
Android で StatusBar.setBackgroundColor/setOverlaysWebView が効かない | targetSdk 35+/36 のエッジツーエッジ | CSS でバーの下に背景を描画し、アイコンの色には SystemBars.setStyle を使用する |
Capacitor 8 へのアップグレード後も android.adjustMarginsForEdgeToEdge が設定に残り、マージンがなくなった | オプションが削除され、無視されている | 削除し、SystemBars と CSS に任せる |
Logcat に Unknown insetsHandling value '...'. Falling back to 'css'. と表示される | SystemBars.insetsHandling が無効 | 現行ドキュメントに記載されている値(css、disable。新しいコアでは native も使用可能)を使う |
| ステータスバーの文字が見えない(白地に白) | スタイルの不一致 | SystemBars.setStyle({ style: SystemBarsStyle.Light }) |
| 初回描画時にレイアウトが跳ねる(Android) | ページのコミット後に SystemBars が viewport-fit を検出する | plugins.SystemBars.initialViewportFitValueHint: 'cover' を設定する(コアの設定宣言を確認し、使用中のバージョンで利用可能か検証してください) |
リソース
- System Bars API:https://capacitorjs.com/docs/apis/system-bars
- Status Bar API(Android 16 に関する注記):https://capacitorjs.com/docs/apis/status-bar
- 8.0 へのアップデート:https://capacitorjs.com/docs/updating/8-0
- Android のエッジツーエッジ:https://developer.android.com/develop/ui/views/layout/edge-to-edge
- WebKit の viewport-fit/env():https://webkit.org/blog/7929/designing-websites-for-iphone-x/