【JavaScript】スマホの画面いっぱいに要素を表示する方法|100vhが効かない原因とdvh・–vhカスタムプロパティ・svh/lvhの使い分けまで解説

スマホで height: 100vh を指定したのに、アドレスバーやタブバーの分だけ要素がはみ出てスクロールが発生する。ヒーローセクションやモーダル背景をピッタリ画面いっぱいにしたいのに思い通りにならない。これはモバイルブラウザの仕様によるもので、多くの開発者がつまずく問題です。

この記事では なぜ100vhではうまくいかないのか を仕組みから説明し、CSSの新しい単位 dvh/svh/lvh とJavaScriptによるカスタムプロパティ回避策の両面から解決策を解説します。

スポンサーリンク

100vhでうまくいかない原因

PCブラウザでは 1vh = ビューポートの高さの1% と直感的に一致します。しかしiOS SafariやAndroid Chromeではブラウザ自身のUIバー(アドレスバー・タブバー・ナビゲーションバー)が画面の一部を占有しており、それが vh の計算方法に影響します。

具体的には iOSのSafariは 1vh をUIバーが非表示の状態(最大ビューポート)を基準に計算します。ページを開いたばかりのUIバーが表示されている状態では、100vh の高さが実際の画面より大きくなり、縦スクロールが発生します。

状況 iOSのvhの基準 結果
UIバー表示中(ページ読み込み直後) UIバーがない状態の高さ 100vhがはみ出る
スクロールしてUIバーが隠れたとき ほぼ一致する 見た目上は問題なし
Chrome(Android) 現在のビューポート高さ dvhと同様に動作
なぜiOS SafariはUIバーなしで計算するのか:
ページ読み込み時にUIバーの表示・非表示が変わると vh の値が変動し、レイアウトが再計算されてガタつきます。Apple はこのガタつきを防ぐために vh を最大ビューポート(UIバーなし)で固定しました。これが意図的な設計であり、「バグ」ではありません。

解決策①:CSS新単位 dvh / svh / lvh(推奨)

この問題を解消するためにCSS仕様に追加された3つのビューポート単位です。2024年現在、主要ブラウザすべてで利用できます。

単位 名称 基準 UIバー表示中 UIバー非表示中
dvh Dynamic Viewport Height 現在の実際のビューポート UIバー分だけ小さい 大きくなる
svh Small Viewport Height UIバーが最大表示のとき(最小ビューポート) ピッタリ合う あまる
lvh Large Viewport Height UIバーが非表示のとき(最大ビューポート) はみ出る ピッタリ合う
vh (従来) iOSは lvh と同等 はみ出る ピッタリ合う
推奨:dvh をフォールバック付きで使う
.hero {
  /* 古いブラウザ向けフォールバック */
  height: 100vh;
  /* dvh に対応していれば上書き */
  height: 100dvh;
}
どの単位を選ぶか:

  • dvh:「常に今見えている画面ぴったり」にしたいとき。スクロールするとレイアウトが再計算されることに注意(アニメーションが発生する場合あり)
  • svh:「UIバーが出ているときも欠けないようにしたい」ヒーローセクションや全画面背景に最適。UIバー非表示時は少し短くなる
  • lvh:従来の vh と同等。iOSでははみ出る問題は残る

ヒーローセクション・モーダル背景には 100svhmin-height: 100svh が最もトラブルが少なくおすすめです。

実用例:ヒーローセクションに svh を使う
/* ヒーローセクション:常に「欠けない」画面いっぱい */
.hero {
  min-height: 100vh;       /* フォールバック */
  min-height: 100svh;      /* UIバーあり状態でも欠けない */
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 全画面モーダルオーバーレイ */
.modal-overlay {
  position: fixed;
  inset: 0;                /* top:0 right:0 bottom:0 left:0 の一括指定 */
  width: 100%;
  height: 100dvh;          /* 現在の表示領域ぴったり */
  background: rgba(0, 0, 0, 0.5);
}

解決策②:JavaScript で –vh カスタムプロパティを設定する

dvh/svh に対応していない古いブラウザへも対応が必要な場合、JavaScriptで window.innerHeight を取得して CSS カスタムプロパティに渡す方法が広く使われています。

–vh カスタムプロパティを設定する
/**
 * window.innerHeight を CSS カスタムプロパティ --vh に設定する
 * iOS Safari で 100vh が正しく計算されない問題の回避策
 */
function setViewportHeight() {
  // 1vh 相当のピクセル値を算出
  const vh = window.innerHeight * 0.01;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
}

// 初回実行
setViewportHeight();

// リサイズ時・向き変更時に再計算
window.addEventListener('resize', setViewportHeight);
window.addEventListener('orientationchange', () => {
  // orientationchange 直後は innerHeight がまだ更新されていない場合がある
  setTimeout(setViewportHeight, 100);
});
CSS側:–vh を使って高さを指定する
.full-screen {
  /* フォールバック:vh が正しく動作する環境用 */
  height: 100vh;

  /* --vh が設定されていればこちらを使う */
  height: calc(var(--vh, 1vh) * 100);
}

/* min-height 版 */
.hero {
  min-height: 100vh;
  min-height: calc(var(--vh, 1vh) * 100);
}
var(--vh, 1vh) のフォールバック値について:
CSS変数の第2引数はフォールバック値です。JavaScriptが実行される前(SSRや初期描画の一瞬)に --vh がまだ設定されていない場合、1vh が使われます。これにより JavaScript 実行前でも表示が崩れません。

リサイズ・向き変更への対応

スマホの向き変更やソフトキーボード表示時にも --vh が変化します。ただしリサイズイベントを毎回発火させると負荷が高いため、デバウンス処理を入れるのが実務的です。

デバウンス付きの完成版
function setViewportHeight() {
  const vh = window.innerHeight * 0.01;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
}

// デバウンス:連続イベントをまとめて1回だけ実行
function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

// 初回
setViewportHeight();

// resize はデバウンス付き(150ms 後に実行)
window.addEventListener('resize', debounce(setViewportHeight, 150));

// orientationchange は 300ms 待つ(値が安定してから取得)
window.addEventListener('orientationchange', () => {
  setTimeout(setViewportHeight, 300);
});
ソフトキーボード表示時の注意:
Androidではソフトキーボードが表示されると window.innerHeight が小さくなり resize が発火します。これにより入力フォームがある全画面UIでレイアウトが崩れる場合があります。visualViewport.height を使うか、入力フォームには --vh を使わないようにするのが実用的な対策です。

補足:-webkit-fill-available(非推奨)

古いiOS Safariで使われていたベンダープレフィックス付きの方法です。現在は dvh/svh で代替できるため新規実装では使う必要はありませんが、既存コードで見かけることがあるため紹介します。

-webkit-fill-available(参考)
.full-screen {
  height: 100vh;                      /* 標準フォールバック */
  height: -webkit-fill-available;     /* iOS Safari 向け(非推奨) */
  height: 100dvh;                     /* 現行の推奨 */
}
-webkit-fill-available は使わないほうが良い理由:

  • 非標準のベンダープレフィックスであり、仕様上の保証がない
  • Chromeなど他のブラウザで期待通りに動作しないことがある
  • iOS 15.4以降は svh/dvh で同等の効果が得られる

ブラウザサポートと実装戦略

手法 iOS Safari Chrome(Android) Chrome(PC) Firefox 必要なJS
100vh(従来) ×(はみ出る) 不要
100dvh iOS 15.4+ Chrome 108+ Firefox 101+ 不要
100svh iOS 15.4+ Chrome 108+ Firefox 101+ 不要
--vh カスタムプロパティ ○(全バージョン) 必要
実装戦略:段階的フォールバックの完成形
.full-screen {
  /* 1. 最古のブラウザ向けフォールバック */
  height: 100vh;

  /* 2. JS が実行されたら --vh カスタムプロパティを使う(iOS 15.4 未満も対応) */
  height: calc(var(--vh, 1vh) * 100);

  /* 3. dvh 対応ブラウザではこちらが優先(@supports で上書き) */
}

@supports (height: 100dvh) {
  .full-screen {
    height: 100dvh;
  }
}
2024年以降の推奨フロー:

  1. まず 100svh(ヒーロー・背景)か 100dvh(モーダル・オーバーレイ)を使う
  2. 100vh をフォールバックとして先に書く
  3. iOS 15.4 未満のサポートが必要な場合のみ --vh JSカスタムプロパティを追加する

Google Analytics などで訪問者のOSバージョンを確認し、iOS 15未満のユーザーがほぼいなければ CSS だけで解決できます。

完成形:フルスクリーンUIコンポーネント

ヒーローセクションとモーダルオーバーレイの2パターンをまとめた実用コードです。

HTML
<!-- ヒーローセクション -->
<section class="hero">
  <h1>キャッチコピー</h1>
  <p>サブテキスト</p>
</section>

<!-- 全画面モーダル -->
<div class="modal-overlay" id="modal" hidden>
  <div class="modal-content">
    <button class="modal-close">閉じる</button>
    <p>モーダル内容</p>
  </div>
</div>
CSS
/* ヒーローセクション:svh で欠けを防ぐ */
.hero {
  min-height: 100vh;
  min-height: calc(var(--vh, 1vh) * 100);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

@supports (min-height: 100svh) {
  .hero { min-height: 100svh; }
}

/* モーダルオーバーレイ:dvh で現在の画面ぴったり */
.modal-overlay {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100vh;
  height: calc(var(--vh, 1vh) * 100);
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

@supports (height: 100dvh) {
  .modal-overlay { height: 100dvh; }
}

.modal-content {
  background: #fff;
  border-radius: 8px;
  padding: 24px;
  max-width: 90%;
  max-height: 80vh;
  overflow-y: auto;
}
JavaScript(–vh 設定 + モーダル制御)
// --vh カスタムプロパティを設定(デバウンス付き)
function setVh() {
  document.documentElement.style.setProperty(
    '--vh', `${window.innerHeight * 0.01}px`
  );
}
setVh();
window.addEventListener('resize', debounce(setVh, 150));
window.addEventListener('orientationchange', () => setTimeout(setVh, 300));

function debounce(fn, ms) {
  let t;
  return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
}

// モーダル制御
const modal = document.getElementById('modal');
document.querySelector('.modal-close').addEventListener('click', () => {
  modal.hidden = true;
});

よくある質問(FAQ)

Qdvh と svh の使い分けがよくわかりません。
A「スクロール中にレイアウトが変わるのが嫌なとき → svh」「常に今見えている画面にぴったり合わせたいとき → dvh」と覚えてください。ヒーローセクションや背景画像は svh、常に画面全体を覆うオーバーレイは dvh が適しています。
QPCでは問題ないのにスマホだけレイアウトが崩れます。
Aこの記事で説明したiOSのvhの仕様が原因です。PCでは 100vh と実際の画面が一致するため問題が起きません。CSS の 100svh か、JavaScript の --vh カスタムプロパティを使うと修正できます。
Qソフトキーボードを開くとレイアウトが崩れます。
AAndroidではソフトキーボードが開くと window.innerHeight が変化し resize が発火するため、--vh が更新されてレイアウトが変わります。visualViewport.height を使うか、入力フォームを含むエリアには固定高さを使わずに flex: 1overflow-y: auto で伸縮させる設計が安定します。
QiOS Safari のバージョンはどこから確認できますか?
AGoogle Analytics(ユーザー → テクノロジー → OS のバージョン)やMixpanel等の解析ツールで確認できます。dvh/svh は iOS 15.4 以降で対応しているため、訪問者のiOSバージョン分布を確認してから対応方針を決めましょう。
QNext.js や Nuxt などのSSR環境でも –vh は使えますか?
A使えますが、サーバーサイドではJavaScriptが実行されないため --vh は設定されません。CSSの var(--vh, 1vh) のフォールバック値 1vh が使われます。useEffect(React)や onMounted(Vue)の中で setVh() を呼ぶことで、クライアントサイドでの初期化後に正しい値が設定されます。

まとめ

対応範囲 推奨する手法
iOS 15.4以降のみでOK CSS 100svh(ヒーロー)/ 100dvh(オーバーレイ)
iOS 15未満も含めて対応 JS --vh カスタムプロパティ + CSS calc(var(--vh,1vh)*100)
将来性・メンテナンス性 CSS新単位を優先し、必要なときだけJSで補う
新規実装の基本方針 100svh を書き、フォールバックに 100vh を置く

要素の高さをJavaScriptで取得・設定する方法は要素の高さを揃える方法(ResizeObserver対応)で、要素の幅・高さの取得プロパティの違いはoffsetWidth・clientWidth・getBoundingClientRectの違いで詳しく解説しています。