結論:100vhはもう既定値にしない
- スマホの100vhは「アドレスバーが引っ込んだ状態」の高さで固定される=実際の画面より数十px大きい
- ファーストビューは min-height:100svh が既定。下端に置いたボタンが隠れなくなる
- 100dvh はスクロール中に高さが毎フレーム動く=ヒーローに使うとガタつく。ドロワーやモーダル専用
- 書き方は 100vh の次行に 100svh を重ねるだけ。@supports もJSも要らない
- JSの --vh ハックは役目を終えた。入力フォーム込みの全画面だけ例外
スマホで確認したらファーストビューの下にあるボタンが画面外に隠れている、スクロールしていないのに縦スクロールバーが出る——height: 100vh を書いた画面で必ずと言っていいほど起きるこの崩れは、CSSのバグではなくビューポート単位の仕様どおりの挙動です。原因はモバイルブラウザのアドレスバーで、直し方は svh / lvh / dvh の使い分けに集約されます。実案件で踏んだ順に、診断のしかたと実務で使っているコードをまとめます。
なぜ100vhはスマホで「画面より大きい」のか
PCでは 100vh = ウィンドウの高さで一致します。ずれるのはモバイルだけで、理由はアドレスバー(とツールバー)がスクロールに応じて出たり引っ込んだりするからです。
100vhは「アドレスバーが引っ込んだ状態」で固定されている
モバイルブラウザは、スクロール中に高さの基準が動くとレイアウトが暴れるため、vh の基準をページ読み込み時から固定しています。そしてその固定値はアドレスバーが最も縮んだ状態=ビューポートが最も広い状態を採用します。これは後述する lvh(large viewport height)と同じ値です。
つまりページを開いた直後、アドレスバーが表示されている状態では、100vh の要素は実際に見えている領域よりアドレスバーの高さぶんだけ下にはみ出しています。ヒーローの下端に置いたスクロール誘導やCTAボタンが隠れるのはこれが理由です。
実測:手元の環境で何px余っていたか
案件で計測したときの値を残しておきます(2026年時点、実機・幅は縦持ち)。
- iPhone / Safari:
lvhとsvhの差はおおむね70〜90px。上のアドレスバーと下のツールバーの合計ぶんです。 - Android / Chrome:差は50〜60px前後。上のアドレスバーのみなのでiOSより小さい。
- ホーム画面に追加したPWA(standalone):差は0。アドレスバーが無いため
vhで崩れません。開発中にPWA表示で確認していると、この不具合を見落とします。
「70px程度なら」と思いがちですが、下端にボタンを置く設計では致命的です。ボタンの高さ(44〜56px)がそのまま画面外に出ます。
30秒で確認するスニペット
実機のDevToolsが使えないときは、確認用の要素を差し込んで3つの単位を並べて出すのが速いです。Safariのコンソールやブックマークレットでもそのまま動きます。
(() => {
const probe = document.createElement('div');
probe.style.cssText =
'position:fixed;top:0;left:0;width:1px;visibility:hidden;pointer-events:none';
document.body.appendChild(probe);
const h = (unit) => {
probe.style.height = '100' + unit;
return Math.round(probe.getBoundingClientRect().height);
};
console.log({
vh: h('vh'), svh: h('svh'), lvh: h('lvh'), dvh: h('dvh'),
innerHeight: window.innerHeight
});
probe.remove();
})();
ここで vh と lvh が同じ値、svh だけが小さければ、想定どおりの挙動です。innerHeight はその瞬間の実表示高さなので、スクロール前は svh 寄り、スクロール後は lvh 寄りになります。
svh / lvh / dvh の違いを1分で
3つとも「ビューポートの高さ」ですが、どの状態を基準にするかが違います。
svh=最も狭い状態(アドレスバーが出ている)
small viewport height。アドレスバーやツールバーがすべて表示された、いちばん狭い状態の高さです。ここに収めておけば、どのタイミングでも要素が画面外にはみ出しません。値は固定なので、スクロール中に高さが変わることもありません。
lvh=最も広い状態(アドレスバーが引っ込んでいる)
large viewport height。vh と同じ値です。あえて lvh と書く場面はほぼありませんが、「vh は lvh のことだ」と理解しておくと原因の切り分けが速くなります。
dvh=いま実際に見えている高さ(動的)
dynamic viewport height。アドレスバーの伸縮に合わせて値がリアルタイムに変わります。常に画面ぴったりになる反面、スクロール中は毎フレーム高さが変化するため、使いどころを選びます。
既定は svh、動かしたい箇所だけ dvh
迷ったら svh です。ファーストビューが少し狭くなるだけで、隠れる事故は起きません。dvh は「開いている間スクロールしない要素」——ドロワーやモーダルに限定します。
100dvhの落とし穴:スクロール中にガタつく
「常に画面ぴったりなら dvh が最強では」と考えて、ヒーローに min-height: 100dvh を入れたことがあります。結果はガタつきでした。
高さが変わる=毎フレーム再レイアウトが走る
スクロールを始めるとアドレスバーが縮み、dvh の値が増えます。100dvh の要素は高さが伸び、その下のコンテンツは押し下げられます。ユーザーから見ると「スクロールしているのに内容が付いてくる」ようなブレになります。中身を flex で天地中央に置いていると、中央位置まで動くので余計に目立ちます。
transitionを書いていると最悪の組み合わせになる
transition: all .3s のような雑な指定が入っていると、この高さ変化にトランジションが乗ります。アドレスバーが縮むたびに0.3秒かけて要素がにゅっと伸びる、という挙動になります。transition の対象は必ずプロパティ名で限定してください。height や min-height をアニメーション対象に含めないだけで防げます。
dvhを使ってよい場面
背面のスクロールを止めた状態で開く全画面要素——ハンバーガーメニューのドロワー、モーダル、画像ビューアです。開いている間はスクロールしない=アドレスバーが動かないので、dvh の弱点が出ません。むしろ画面ぴったりに閉じてくれる利点だけが残ります。
実務で使っているコード(そのまま流用可)
ヒーローセクション:2行重ねるだけ
フォールバックは @supports ではなく、単純な上書きで済みます。svh を解釈できないブラウザは2行目を捨てて1行目を使うため、これだけで両対応です。
.hero {
min-height: 100vh; /* 未対応ブラウザ用のフォールバック */
min-height: 100svh; /* 対応ブラウザはこちらで上書きされる */
display: flex;
flex-direction: column;
justify-content: center;
}
height ではなく min-height にしておくのも重要です。文字量が増えたときや、多言語サイトで翻訳後に行数が伸びたときに、height 固定だと中身がはみ出します。
ヘッダーを除いた残り全部を使いたいとき
固定ヘッダーがある場合、calc() で引きます。ヘッダー高さはCSS変数にしておくと、ブレークポイントごとの変更が1か所で済みます。
:root { --header-h: 64px; }
@media (max-width: 767px) { :root { --header-h: 56px; } }
.hero {
min-height: calc(100vh - var(--header-h));
min-height: calc(100svh - var(--header-h));
}
ドロワー/モーダル:ここは dvh
.drawer {
position: fixed;
inset: 0;
height: 100vh;
height: 100dvh; /* 開いている間は画面ぴったり */
overflow-y: auto;
}
背面のスクロールロックはposition:fixed方式で
overflow: hidden だけではiOSで背面が動きます。加えて body に overflow-x: hidden を常時入れている実装は、ページ内の position: sticky をすべて無効化するので要注意です(別記事で詳述しています)。スクロール位置を保持したまま止める書き方は次のとおりです。
let scrollY = 0;
function lockScroll() {
scrollY = window.scrollY;
document.body.style.position = 'fixed';
document.body.style.top = '-' + scrollY + 'px';
document.body.style.width = '100%';
}
function unlockScroll() {
document.body.style.position = '';
document.body.style.top = '';
document.body.style.width = '';
window.scrollTo(0, scrollY);
}
キーボードが出ると崩れる、をどう扱うか
入力欄のある全画面レイアウトでは、ソフトキーボードの表示でもう一段ややこしくなります。
既定ではキーボードでビューポートは縮まない
多くのブラウザの既定では、キーボードが出てもレイアウトビューポートは縮まず、画面全体が押し上げられる(ビジュアルビューポートだけが変わる)挙動になります。dvh の値もこのとき変わらないのが基本です。「キーボードで 100dvh が縮む前提」で組むと環境差で破綻します。
縮ませたいときは meta viewport で明示する
チャットUIのように「入力欄をキーボードの上に貼り付けたい」場合は、ビューポート側の挙動を指定します。
<meta name="viewport"
content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
この指定を入れるとキーボード表示でビューポート自体が縮み、100dvh がキーボードを除いた高さになります。ただし対応状況に差があるため、この挙動に依存する画面は必ず実機で確認してください。汎用のコーポレートサイトやLPでは、そもそも設定しないほうが安全です。
JSの –vh ハックはもう外していい
window.innerHeight * 0.01 をCSS変数に書き戻す、いわゆる --vh ハックが長く使われてきました。現在は原則不要です。外す判断基準を残しておきます。
- そもそも欠点が多い:
resizeのたびに再計算が走る、iOSのスクロール中はイベントが発火せず値が古いまま、初回描画で一瞬ずれる、といった副作用があります。svh/dvhはこれらをCSSだけで解決します。 - 外す条件:対応ブラウザが揃っている案件なら、
height: calc(var(--vh) * 100)を上の2行重ねに置換し、JSを削除します。既存サイトの改修では、--vhを参照している箇所をgrepしてから消してください。CSSだけ直してJSを残すと、無害ですが無駄な再計算が残ります。 - 残してよい例外:キーボード表示に合わせて厳密に高さを合わせたい入力画面。ここは
visualViewportAPIを使ったほうが正確で、--vhハックの正統な後継になります。
公開前チェックリスト
- ファーストビューを
100svhにしたうえで、実機のSafari/Chromeでページを開いた直後(アドレスバーが出ている状態)に下端要素が見えているか。 - スクロールを始めたときにヒーローの中身が上下に動かないか(動くなら
dvhが残っている)。 transitionにallを使っていないか。- ドロワーを開いた状態で背面がスクロールしないか。iOSは特に。
- PWA/ホーム画面追加ではなく、通常のブラウザタブで確認したか。
- 文字量が増えても崩れないよう
heightではなくmin-heightを使っているか。多言語サイトでは翻訳後に行数が増えるため必須です。多言語サイトの作り方でも同じ観点に触れています。
よくある質問
Q1. 100vhと100%は何が違いますか?
100% は親要素の高さに対する割合なので、親に高さが無ければ効きません(html, body { height: 100% } が必要になるのはこのためです)。100vh はビューポート基準なので親に依存しません。逆に言えば、親の高さを継承したい場面で vh を使うと、入れ子の途中でスクロールが二重になる原因になります。
Q2. svhとdvhはどのくらい使えますか?
主要モバイルブラウザの現行版では利用できます。ただし社内で古い端末を検証対象に含めている場合や、企業サイトで古いWebViewからの閲覧がある場合は、本文のとおり 100vh を1行目に置くフォールバックを必ず残してください。2行書くコストはゼロに近く、外す理由がありません。
Q3. 100vhをやめたらファーストビューが狭く見えます。どうすべきですか?
svh は「いちばん狭い状態」なので、スクロール後は下に余白ができたように見えます。これは不具合ではなく想定どおりです。デザイン上どうしても画面いっぱいに見せたいときは、背景だけを lvh(=vh)で敷き、テキストとボタンを含むコンテンツ側を svh の範囲に収める、という分離が有効です。背景がはみ出すぶんには実害がありません。
まとめ
100vh がスマホでずれるのは、vh がアドレスバーの引っ込んだ状態(=lvh)で固定されているためです。既定を 100svh に置き換え、100vh をフォールバックとして1行上に残す。この2行だけで、下端のボタンが隠れる事故はなくなります。100dvh は常に画面ぴったりになる代わりにスクロール中へ高さ変化を持ち込むため、ドロワーやモーダルなど「開いている間スクロールしない要素」に限定します。JSの --vh ハックは役目を終えており、キーボード連動の入力画面だけが例外です。表示崩れの検証は必ず実機のブラウザタブで、ページを開いた直後の状態から行ってください。
なお、ファーストビューの体感速度はCSS以前にサーバーの応答時間で決まる部分も大きく、Web制作者が選ぶサーバー比較やWordPressのサーバー移行手順も併せて見直すと効果が出やすい領域です。
多言語・ベトナム語サイトの制作でお困りではありませんか?
フォント・文字化け・WordPress多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。
多言語サイト制作の詳細を見る →すぐに相談したい方は お問い合わせフォームへ