モバイルで下のボタンがホームインジケーターに重なって押しにくいことはありませんか。
端末が変わるとドロワーやトーストの位置が少しずれて見えることもあります。
その数ピクセルが使いやすさに直結します。
この記事では env(safe-area-inset-bottom) を使って、ノッチやジェスチャーバーと重ならない配置にする方法を説明します。
前提設定から実装手順、max() や clamp() を使った調整までを順に見ていきます。
対応していない環境でも崩れにくい書き方も紹介します。
読み終えるころには「見える」「押せる」「ずれない」レイアウトを短時間で作れるようになります。
今日の案件から使える、わかりやすいコードを用意します。
セーフエリアとenv()について

ノッチは画面上部の切り欠き部分です。
フロントカメラやセンサーを収めるためのスペースで、画面の一部が欠けて見えます。
端末ごとにサイズや形が違います。
ホームインジケーターは画面下部の細いバーです。
iPhoneや一部Androidで、スワイプ操作の起点になります。
このバーに近い領域は指で隠れやすく、ボタンが押しづらくなります。
セーフエリアはノッチやホームインジケーターに重ならない安全な表示範囲です。
端末が「ここに配置すると見やすい」と教えてくれる領域です。
CSSの env() 関数を使うと、この値をそのまま余白にできます。
/* 例:フッターに下端の安全余白を足します */
.page-footer {
padding-bottom: env(safe-area-inset-bottom);
}
端末が env(safe-area-inset-bottom) の値を提供します。
これにより、ホームインジケーター上にCTAが重なりにくくなります。
未対応の端末でも崩れないよう、フォールバックを入れておくと安心です。
.page-footer {
padding-bottom: env(safe-area-inset-bottom, 16px);
}
次に、似た名前の関数と違いを整理します。var() は開発者が定義したCSS変数を参照します。env() はOSやブラウザが提供する環境変数を参照します。
用途が異なるため、目的に合わせて使い分けます。
:root { --space-m: 16px; }
.section {
padding-bottom: var(--space-m);
}
実装のコツは、「まず下端を守る」ことです。
ホームインジケーター付近はもっとも操作が集中します。
最初に下端の余白だけ入れて、体験を早く安定させます。
/* 固定CTAを下端に安全配置 */
.c-ctaFixed {
position: fixed;
left: 0;
right: 0;
bottom: env(safe-area-inset-bottom, 0);
padding: 12px;
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0));
}
この設定で、見た目の高さは保ったまま、端末ごとの安全余白を自動で確保できます。
viewport設定と表示モードの前提
セーフエリアを正しく使うには、まずビューポートを整える必要があります。
ここが合っていないと env(safe-area-inset-bottom) が常に 0 になることがあります。
最低限の meta viewport
<meta name="viewport" content="width=device-width, initial-scale=1">ズーム倍率と幅をそろえます。
これがないと相対計算が不安定になります。
端まで描画するための viewport-fit=cover
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">画面の端までコンテンツを広げます。
cover にしたら、自分で安全余白を付ける前提になります。
全体に安全余白を一括で付与
/* レイアウトの親要素に適用 */
.l-app {
padding:
env(safe-area-inset-top, 0)
env(safe-area-inset-right, 0)
env(safe-area-inset-bottom, 0)
env(safe-area-inset-left, 0);
}
非対応端末では 0 として扱われます。
固定UIにだけ安全余白を足す
/* 下固定CTA */
.c-ctaFixed {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0));
}
/* 下寄せトースト */
.c-toast {
position: fixed;
left: 50%;
bottom: calc(16px + env(safe-area-inset-bottom, 0));
transform: translateX(-50%);
}
見た目の余白 + セーフエリアの加算が基本です。bottom を直接 env() にするより、padding で持ち上げるほうが安定します。
非対応や差分に備えるフォールバック
/* まず固定値で安全に */
.c-ctaFixed { padding-bottom: 16px; }
/* 対応環境だけ強化 */
@supports (padding: max(0px)) {
.c-ctaFixed {
padding-bottom: max(16px, calc(12px + env(safe-area-inset-bottom, 0)));
}
}
/* 値が大きすぎる端末は上限を設定 */
.c-ctaFixed {
padding-bottom: clamp(16px, calc(12px + env(safe-area-inset-bottom, 0)), 40px);
}
通常のブラウザはツールバーの出入りで見え方が変わります。縦横の回転とスクロール後の位置を実機で確認します。
PWAやインストール済みWebアプリはセーフエリア値が変わりやすいです。固定ボトムUIは見た目の余白に env(safe-area-inset-bottom) を足す前提で設計します。
アプリ内WebViewは実装差が大きいです。OS分岐より @supports で機能があるときだけ強化します。
要点は三つです。最初に viewport-fit=cover を決めます。固定UIは padding で持ち上げます。固定値で始めて、@supports を通った環境だけ env() を足します。
該当するパーツの作り方
ドロワーメニュー
まずはドロワーです。右からスライドし、内側の上下に env(safe-area-inset-*) を足して、ノッチとホームインジケーターを避けます。動作イメージを以下で確認できます。
See the Pen iPhoneのノッチやセーフエリアに対応 by Suzuki Kazuma (@build_suzuki) on CodePen.
次にフッターとトーストです。どちらも位置は下端に固定し、見た目の余白 + env(safe-area-inset-bottom) を padding で加算します。bottom を直接 env にせず、padding で持ち上げると崩れにくいです。
追従CTA
<div class="c-cta-fixed" role="region" aria-label="固定CTA">
<a class="c-cta-fixed__btn" href="/contact">お問い合わせ</a>
</div>
.c-cta-fixed {
position: fixed;
left: 0;
right: 0;
bottom: 0; /* 位置は0で固定 */
padding: 12px 16px;
/* 見た目の余白 + セーフエリア。上限で暴れを抑えます */
padding-bottom: clamp(16px, calc(12px + env(safe-area-inset-bottom, 0)), 40px);
background: #111;
color: #fff;
box-shadow: 0 -2px 10px rgba(0,0,0,.08);
}
.c-cta-fixed__btn {
display: block;
width: min(600px, 92vw);
margin: 0 auto;
text-align: center;
line-height: 48px;
font-weight: 700;
}
- まずは固定値で安全にし、対応環境では
env()を採用します。 - 値が過大になる端末は
clamp()で上限を決めます。
トースト
<div class="c-toast" role="status" aria-live="polite">
保存しました。
</div>
.c-toast {
position: fixed;
left: 50%;
transform: translateX(-50%);
/* 下端距離 + セーフエリア */
bottom: calc(16px + env(safe-area-inset-bottom, 0));
padding: 10px 14px;
background: #222;
color: #fff;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,.15);
opacity: 0;
pointer-events: none;
transition: opacity .2s ease, transform .2s ease;
}
.c-toast.is-active {
opacity: 1;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
}
- アニメーションの終点も同じ式(
calc(16px + env(...)))にそろえるとズレにくいです。 - フォーム送信後などは短時間だけ
.is-activeを付け外しします。
フォールバックと上限管理でUIを安定化
env() に依存しすぎない設計にします。
まず固定値で形を作り、対応環境だけ @supports で強化し、max() と clamp() で最小値と上限を管理します。bottom は 0 に保ち、padding に一度だけ env() を足す流れが安全です。
固定値から始めて対応環境だけ強化
.c-cta-fixed { padding-bottom: 16px; }
@supports (padding: max(0px)) {
.c-cta-fixed {
padding-bottom: max(16px, calc(12px + env(safe-area-inset-bottom, 0)));
}
}
上限を決めて過大な余白を防ぐ
.c-cta-fixed {
padding-bottom: clamp(16px, calc(12px + env(safe-area-inset-bottom, 0)), 40px);
}
二重加算を避ける計算順序にする
/* 悪い例 */
.c-bad {
bottom: env(safe-area-inset-bottom, 0);
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0));
}
/* 良い例 */
.c-good {
bottom: 0;
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0));
}
機能検出で段階的に強化
.c-toast { bottom: 16px; }
@supports (bottom: calc(16px + env(safe-area-inset-bottom, 0))) {
.c-toast { bottom: calc(16px + env(safe-area-inset-bottom, 0)); }
}
トークンと合成して保守性を高める
:root { --space-m: 12px; }
.c-drawer__panel {
padding-bottom: calc(var(--space-m) * 2 + env(safe-area-inset-bottom, 0));
}
固定値で土台を作り、対応環境では env() を重ねて最小値を担保しつつ、clamp() で上限を縛ります。
位置は bottom: 0 のままにして、見た目の余白へ一回だけ env() を加えることで、端末差やWebViewの挙動差でも崩れにくくなります。
再利用できるようにする
毎回計算式を書く手間を減らすために、カスタムプロパティを用意します。
既存コンポーネントにクラスを足すだけで、簡単に実装できます。
:root {
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--safe-bottom: env(safe-area-inset-bottom, 0);
--safe-top: env(safe-area-inset-top, 0);
}
/* 下端の安全余白を付与(上限つき) */
.u-safe-bottom {
padding-bottom: clamp(var(--space-3), calc(var(--space-3) + var(--safe-bottom)), 40px);
}
/* 上下の安全余白を付与(ヘッダーやドロワーに) */
.u-safe-block {
padding-top: calc(var(--space-3) + var(--safe-top));
padding-bottom: clamp(var(--space-3), calc(var(--space-3) + var(--safe-bottom)), 40px);
}
/* 下寄せコンポーネントの位置調整(トーストなど) */
.u-bottom-offset {
bottom: calc(var(--space-4) + var(--safe-bottom));
}
あとは既存の要素にクラスを足すだけです。
<!-- 固定CTAを安全に -->
<div class="c-cta-fixed u-safe-bottom">
<a class="c-cta-fixed__btn" href="/contact">お問い合わせ</a>
</div>
<!-- ドロワーパネルの内側に上下の安全余白 -->
<aside class="c-drawer__panel u-safe-block">…</aside>
<!-- トーストの終点位置を安全に -->
<div class="c-toast u-bottom-offset">保存しました。</div>
ポイントは二つです。まず 位置は bottom: 0 を基本 とし、見た目の余白に安全分を足します。
次に 上限は clamp で管理 し、値が大きい端末でも間延びしないようにします。
これでサイト全体へ一括適用しやすくなります。
実機での確認ポイント
まずは現在の余白が正しく効いているかを、端末を跨いで同じ手順で確認します。
iPhoneとAndroidの実機で、縦横の回転、アドレスバーの表示・非表示、スクロール後の戻りを試し、固定フッターやドロワー、トーストがホームインジケーターやノッチに重ならないかを見ます。
フォームにフォーカスしてキーボードを出し、閉じた直後に位置がずれないかも確かめます。
確認を素早くするために、デバッグ用の見た目を一時的に足します。セーフエリア分だけ色を変えて「どれだけ持ち上がっているか」を一目で把握します。
/* デバッグ用。安全余白のぶんだけ背景色を重ねて見えるようにします */
.u-debug-safe {
background:
linear-gradient(#ffe8e8, #ffe8e8) top / 100% env(safe-area-inset-top, 0) no-repeat,
linear-gradient(#e8f6ff, #e8f6ff) bottom / 100% env(safe-area-inset-bottom, 0) no-repeat;
}
値そのものを確認したいときは、CSSカスタムプロパティに env を受けて、JavaScriptで読み取ります。
計測した値が 0 のままなら、viewport-fit=cover や適用要素を見直します。
:root {
--safe-top: env(safe-area-inset-top, 0);
--safe-bottom: env(safe-area-inset-bottom, 0);
}
// コンソールで現在のセーフエリアを確認します
const cs = getComputedStyle(document.documentElement);
console.log('safe-top:', cs.getPropertyValue('--safe-top').trim());
console.log('safe-bottom:', cs.getPropertyValue('--safe-bottom').trim());
固定UIの最終確認では、bottom: 0 を維持したまま padding-bottom に env を一度だけ加算しているかを見ます。
二重に入っていると過大な余白になります。
アニメーションを使っている要素は、終点の bottom や transform がセーフエリア込みの位置にそろっているかをチェックします。
最後に、リンクをタップしてページ遷移した後、戻る操作でも位置が再計算されて崩れないかまで確認します。
まとめ
セーフエリア対応は、ほんの数ピクセルの違いで操作のしやすさが変わります。
まずは viewport-fit=cover を設定し、位置は bottom: 0 のままにします。
余白は padding に env(safe-area-inset-bottom) を加えて持ち上げるのが安定します。
固定フッターやトーストは、余白を「見た目+セーフエリア」で計算する形にすると、どの端末でも自然に見えます。
ドロワーの場合は、上下の padding に env() を加えることで、ノッチやホームインジケーターを避けながら表示できます。
未対応の環境には固定値を使い、対応している環境だけ @supports で強化します。
さらに max() や clamp() を組み合わせて、最小値と上限を決めておくと安心です。









