100vhがスマホでずれる原因と、dvh/svh/lvhの正しい使い分け【CSS実例】

100vhがスマホでずれる原因と、dvh/svh/lvhの正しい使い分け【CSS実例】

結論: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を残すと、無害ですが無駄な再計算が残ります。
  • 残してよい例外:キーボード表示に合わせて厳密に高さを合わせたい入力画面。ここは visualViewport APIを使ったほうが正確で、--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多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。

多言語サイト制作の詳細を見る →

すぐに相談したい方は お問い合わせフォームへ

関連記事

関連記事(コーディング実務Tips)

Web制作Tips(コーディング実務)カテゴリの最新記事