はじめに
モーダルやドロワーの中にフォームを置くと、下の方の入力欄がキーボードに隠れて見えなくなる。iPhone対応の定番トラブルとしてよく語られる話で、対策としてwindow.visualViewportを使ったコードがあちこちで紹介されています。
実際にその状況を作って実機で確認したところ、入力欄は隠れませんでした。iOS Safariが自分で画面を持ち上げて、フォーカスした入力欄を見える位置に出してくれます。この記事では、検証に使った構成と実機で起きたこと、それでもvisualViewportが必要になる場面を整理します。デモページはそのまま公開しているので、お手元のiPhoneでも同じことを確認できます。
検証した構成
「入力欄が隠れる」と言われるときの典型的な組み合わせを、そのまま再現しました。業務アプリのモーダルでよく組まれる形です。
1. 画面いっぱいのオーバーレイを position: fixed で作る
高さは 100%。キーボードが出ていない状態ではフォームがちょうど1画面に収まる。
2. オーバーレイの中にスクロール領域を作らない
overflow: hidden。中身がはみ出しても内側でスクロールする余地はない。
3. モーダル表示中は背景のスクロールを固定する
body を position: fixed にする定番の背景スクロール対策を入れておく。
この3つが揃うと、ブラウザが入力欄を見せようとしても動かせる要素が何もない状態になります。理屈のうえでは、いちばん下の入力欄をタップしたらキーボードの裏に隠れたままになるはずでした。
.overlay {
position: fixed;
inset: 0; /* レイアウトビューポート基準 */
}
.sheet {
height: 100%;
overflow: hidden; /* 内側にスクロールできる領域がない */
}
実機で試せるデモ
キーボードによるビューポートの変化は、記事内の小さい枠では再現できません。お手元のiPhoneで以下のリンクを開いて確認してください。
どちらのページにも、画面上部に黒い帯で実測値を表示しています。window.innerHeightとビジュアルビューポートの高さ、そのズレ、そこから求めたキーボードの高さ、さらに送信ボタンと閉じるボタンが見えているかどうかが、リアルタイムで変わります。入力欄をタップして数値の動きを眺めてみてください。
実機で起きたこと
iPhone実機のSafari(2026年7月に確認)で、いちばん下の入力欄をタップした結果です。
| 確認したこと | 結果 |
|---|---|
| フォーカスした入力欄 | 隠れない。キーボードの上に表示される |
| その下にある送信ボタン | 隠れない。そのままタップできる |
| 上部のヘッダーと閉じるボタン | 画面の外へ押し出される。ただしスクロールすれば戻せる |
visualViewport.offsetTop |
0より大きくなる。画面全体が持ち上がっている |
スクロールできる要素を用意せず、背景も固定してあるのに、画面は動きました。つまりiOS Safariは、ページの中身をスクロールしているのではなく、見えている範囲(ビジュアルビューポート)自体をずらして入力欄を見せています。
なぜ隠れないのか
iOS Safariはソフトキーボードが開いたとき、ページ全体のサイズ(レイアウトビューポート)は変えずに、実際に見えている範囲(ビジュアルビューポート)だけをキーボードの高さぶん縮めます。
このとき、フォーカスされた入力欄がキーボードの裏に来てしまう場合は、ビジュアルビューポートをレイアウトビューポートの中で上へずらします。ずれた量がvisualViewport.offsetTopです。position: fixedで置いたオーバーレイもこのずれに乗って一緒に持ち上がるため、入力欄が見えるようになります。CSSやJavaScriptの助けは要りません。
持ち上がったぶん、オーバーレイの上端は画面の外へ出ます。ヘッダーの閉じるボタンが消えて見えるのはこれが理由です。指でスクロールすればビジュアルビューポートは元の位置に戻せるので、操作不能にはなりません。
visualViewport対応が必要な場面
ではvisualViewportは不要かというと、そうではありません。ブラウザ任せで済むのは「フォーカスした入力欄を見せる」ところまでで、次のような要件は自分で面倒を見る必要があります。
画面下に固定した送信バーやフッターを保ちたい
キーボード表示中も下端に張り付かせたい要素は、持ち上がりの影響をそのまま受けます。こちらは症状も対策も別で、iPhone Safariでfixedフッターがキーボードに隠れる原因と直し方で扱っています。
ヘッダーを画面外に出したくない
閉じるボタンやステップ表示を常に見せたい画面では、持ち上がりを許さずにシート自体を縮める必要があります。対策版のデモがこの実装です。
入力欄を画面の中央に寄せたい
ブラウザ任せだと入力欄はキーボードのすぐ上に来ます。候補リストやエラーメッセージを下に出すUIでは、それでは足りないことがあります。
シート自体をビジュアルビューポートに合わせるコードは次のようになります。高さを合わせ、ずれた量をtranslateYで打ち消すと、キーボードが出ても画面いっぱいに収まったままになります。
const vv = window.visualViewport;
const sheet = document.querySelector('.sheet');
function syncSheet() {
if (!vv) return;
sheet.style.height = `${vv.height}px`;
sheet.style.transform = `translateY(${vv.offsetTop}px)`;
}
if (vv) {
vv.addEventListener('resize', syncSheet);
vv.addEventListener('scroll', syncSheet);
}
この方法でシートを縮めると中身が入りきらなくなるため、内側にoverflow-y: autoのスクロール領域を用意し、フォーカス時にscrollIntoViewで寄せる処理も合わせて入れます。
body.addEventListener('focusin', (event) => {
const target = event.target;
if (!target.matches('input, textarea')) return;
setTimeout(() => {
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
}, 300);
});
遅延を入れているのは、focusの直後だとまだキーボードが開ききっておらず、縮む前の座標でスクロール位置が決まってしまうためです。
対策コードを入れる前に測る
この記事で伝えたいのはここです。visualViewportを使った対策は、必要な画面に入れれば効きますが、必要でない画面に入れるとブラウザ側の調整と自前の補正が二重にかかり、かえって表示が暴れることがあります。ネット上の対策コードには数年前に書かれたものも多く、当時の挙動を前提にしている場合があります。
まずは自分の画面で何が起きているかを測ってください。数行で確認できます。
const vv = window.visualViewport;
vv.addEventListener('resize', () => {
console.log('vv.height', vv.height);
console.log('vv.offsetTop', vv.offsetTop);
console.log('keyboard', window.innerHeight - vv.height - vv.offsetTop);
});
iOSのバージョンや端末によって挙動が変わる可能性は残ります。この記事の結果も特定の1台で確認したものなので、デモページの実測パネルでお手元の環境の値を見比べてみてください。
まとめ
モーダル内の入力欄がキーボードに隠れる、という前提で検証を始めましたが、実機では隠れませんでした。iOS Safariはビジュアルビューポート自体を上へずらすことで、position: fixedのオーバーレイの中にある入力欄も見える位置に出してくれます。スクロールできる領域がなくても、背景を固定していても同じでした。
代わりに起きるのは、画面全体が持ち上がってヘッダーが画面外へ出ることです。閉じるボタンや固定フッターを常に見せたい画面では、visualViewportの値にシートを追従させる実装が今も要ります。逆に、ふつうのフォームを置くだけのモーダルなら、対策コードを足す前に一度実機で測ってみるのがおすすめです。何も書かなくても成立しているかもしれません。
関連するUI事例
オーバーレイの中にフォームを置く構成で、キーボードまわりの確認が必要になる代表的なUI事例です。
同じシリーズの関連記事もあわせてどうぞ。