ノッチやホームバーに隠れないゲーム画面の作り方
遊びをつくる執筆にClaudeを使用しています。

スマートフォンの画面端には、カメラ用のノッチ(切り欠き)やダイナミックアイランド、そしてホームバー(ホームインジケーター)が存在します。これらはOSが優先的に使う領域であり、ゲームのUIやボタンをそこに配置すると、隠れたり誤タップを誘発したりします。「なぜかボタンが反応しない」「スコアが欠ける」といった問題の多くは、この安全領域(セーフエリア)の無視が原因です。 ブラウザゲームの場合、ネイティブアプリと違ってビルドツールが自動で対処してくれません。CSS変数一つとビューポート設定の組み合わせで解決できますが、何をどの順番で設定するかを知らないと、対策したつもりでも特定の端末だけ崩れたままになります。この記事では、HTMLとCSSだけで完結するセーフエリア対応の手順と、判断に迷いやすい場面の考え方を具体的に説明します。
セーフエリアとは何か、どの領域を指すのか
セーフエリアとは、OSのシステムUIと重ならず、コンテンツを安全に表示できる矩形領域です。iOSはこの概念を safe-area-inset-* という環境変数でCSSに公開しています。Androidも画面の切り欠きを持つ端末では同様の仕組みをChrome経由でサポートしています。
具体的な領域は端末ごとに異なります。iPhone 14以降のダイナミックアイランドは上部中央を削る形になっており、古いノッチ型とは形状が違います。ホームバーは画面下部に常時表示される横長の領域で、iOS・Androidともに下からのスワイプでホーム画面に戻るために使われます。これらの「食われる量」を数値として取得できるのが env(safe-area-inset-top) などの変数です。
対応が必要な四方向は次のとおりです。
- safe-area-inset-top:ノッチ・ステータスバー側(上端)
- safe-area-inset-bottom:ホームバー側(下端)
- safe-area-inset-left / safe-area-inset-right:横持ち(ランドスケープ)時のノッチ回り込み
縦持ち専用のゲームでも、左右のinsetがゼロになるとは限りません。端末によって小さな値が入る場合があるため、四方向すべてを変数で指定しておくのが無難です。
対応に必要な二つの前提設定
env(safe-area-inset-*) を機能させるには、HTMLの<meta name="viewport">に viewport-fit=cover を追加することが必須です。これがない場合、ブラウザはノッチを避けてコンテンツを内側に押し込む「contain」モードで動作し、変数はすべてゼロを返します。
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">cover に変更するとコンテンツがノッチの下まで広がります。その代わり、何も対策しないとUIがノッチと重なります。つまり「広げてから避ける」という二段構えが必要です。
次に、ゲームのルートとなる要素(多くは<canvas>や全画面を覆う<div>)に対して、セーフエリア分のパディングを加えます。
#game-container {
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);
}ただし、背景色や装飾は画面端まで伸ばしたいが、操作可能な要素だけ内側に収めたい、というケースが多いです。その場合は背景を持つ外側の要素にはpaddingを使わず、ボタンやスコア表示など内側のUI要素のpositionやmarginにセーフエリア変数を加算する方法が適しています。
UIレイアウト別の実装パターン
固定ポジションのボタンを置く場合
position: fixed または position: absolute で画面端に貼り付けるボタンは、bottom や top の値にセーフエリアを加算します。
#pause-button {
position: fixed;
top: calc(16px + env(safe-area-inset-top));
right: 16px;
}
#jump-button {
position: fixed;
bottom: calc(24px + env(safe-area-inset-bottom));
right: 24px;
}calc() の中で加算することで、デザイン上の余白とセーフエリアの両方を確保できます。セーフエリアがゼロの端末(ノッチなし)でも calc(24px + 0px) となり、通常のマージンとして機能します。
canvas 要素にゲーム描画している場合
canvas 自体はCSS paddingに対応していません。描画領域を変えずにUIだけ内側に収めるには、canvas を position: absolute で画面いっぱいに広げておき、その上にHTMLのUIレイヤーを重ねる構成が扱いやすいです。canvas には背景役割を持たせ、タップ判定はHTML要素側で行います。
純粋にcanvasだけで完結させたい場合は、JavaScriptで getComputedStyle(document.documentElement).getPropertyValue('--sat') のようにCSS変数経由で取得するか、CSS.supports() で環境変数の値を読む方法があります。しかし実装がやや複雑になるため、HTMLレイヤーを重ねる構成の方が多くの場面で現実的です。
スコアや残機など常時表示の情報
画面上部に固定するスコア表示は、margin-top か padding-top にセーフエリアを含めます。特に横持ち対応をしている場合、ランドスケープ時は左側にノッチが来る端末があります。safe-area-inset-left をスコアの left に加えておくと、ノッチの下にテキストが潜り込みません。
確認と検証の進め方
実機がない場合、Chrome DevToolsのデバイスモードで一定程度確認できます。デバイス一覧からiPhone 14以降を選ぶと、ダイナミックアイランドを模したノッチ表示がされ、env(safe-area-inset-*) の値もある程度反映されます。ただしDevToolsのシミュレーションは完全ではなく、実機で確認するとズレが見つかることがあります。
特に確認が必要な場面は次のとおりです。
- 縦持ちから横持ちへの回転時:insetの値が左右に移動し、レイアウトが崩れやすい
- フルスクリーンAPIを使った場合:フルスクリーン中はステータスバーが消えてinsetがゼロになる端末があり、通常表示と値が変わる
- PWAとしてホーム画面に追加した場合:display: standalone のときはブラウザのアドレスバーがなくなり、insetの計算が変わることがある
これらは条件によって挙動が変わるため、対象とするプレイ環境(ブラウザで遊ぶか、ホーム追加を想定するかなど)を先に決めて検証範囲を絞ると効率的です。
失敗しやすいポイントと判断基準
viewport-fit=cover を忘れるのが最も多い見落としです。paddingを書いても変数がゼロのままになり、「対応したのに効かない」という状態になります。必ずmetaタグの変更とセットで実装してください。
背景がノッチ領域で途切れる問題は、cover にしているのに外側要素の背景色が足りていない場合に起きます。背景は html または body 要素にも設定しておくと、セーフエリア外の隙間を埋められます。
env() の古いフォールバック記法として constant(safe-area-inset-*) が存在しますが、これはiOS 11の初期バージョン向けの古い構文です。現在の主流端末では env() だけで問題ありませんが、念のため両方書きたい場合は constant() を先に、env() を後に記述します。後に書いた方が有効なブラウザに採用されます。
セーフエリアを余白の代わりに使わないことも重要です。ノッチがない端末ではinsetがゼロになるため、デザイン上必要な余白はセーフエリアとは別に padding や margin で確保してください。calc(16px + env(safe-area-inset-bottom)) のように加算するのが正しい使い方です。
次の行動
まず手元のHTMLに viewport-fit=cover が入っているか確認し、ない場合は追加してください。次に、画面端に配置しているボタンやUIの bottom / top に calc() とセーフエリア変数を追記し、Chrome DevToolsのデバイスモードでiPhone 14以降を選んで見た目を確認してください。ブラウザゲームとしてアソバースに投稿する場合、スマートフォンでの見え方はそのままプレイヤーの第一印象になるため、実機またはシミュレーターでの最終確認を投稿前の工程に組み込んでおくと安心です。

