CSS で画面いっぱいに広がるセクションを作ろうとして height: 100vh と書いたのに、スマートフォンで見ると下がはみ出してスクロールしてしまう。あるいは width: 100vw にしたら、なぜか横スクロールバーが出てしまった。どちらもビューポート単位の「基準」を理解していないと必ずつまずくポイントです。この記事では vw / vh / vmin / vmax が何を基準にしているのかを整理し、スマホのアドレスバー問題を解決する dvh / svh / lvh という新しい単位までを、動かせるデモ付きで解説します。
目次
ビューポート単位とは
ビューポート単位は、ブラウザの表示領域(ビューポート)のサイズを基準にした長さの単位です。ビューポートとは、ページが表示されている窓の部分のこと。パソコンならブラウザウィンドウのうちツールバーなどを除いた領域、スマートフォンなら画面のうちページが描画されている領域を指します。
もっとも基本的なのが vw と vh です。1vw はビューポートの幅の1%、1vh はビューポートの高さの1%を表します。つまり幅 1200px のビューポートなら 1vw は 12px、50vw は 600px、100vw は 1200px です。画面サイズが変われば計算結果も自動的に変わるので、メディアクエリを書かなくても画面に追従するサイズ指定ができます。
.hero {
/* ビューポートの高さいっぱい */
height: 100vh;
}
.sidebar {
/* ビューポート幅の 25%。1200px の画面なら 300px */
width: 25vw;
}
ビューポート単位には、幅と高さのどちらか一方を基準にする vmin / vmax もあります。4つの単位が何を基準にしているかを整理すると次のとおりです。
| 単位 | 基準になる長さ |
|---|---|
vw | ビューポートの幅の1% |
vh | ビューポートの高さの1% |
vmin | ビューポートの幅と高さのうち、小さいほうの1% |
vmax | ビューポートの幅と高さのうち、大きいほうの1% |
たとえばビューポートが 1200px × 800px のとき、1vmin は小さいほうの 800px の1%で 8px、1vmax は大きいほうの 1200px の1%で 12px になります。スマートフォンを縦に持っているときは幅のほうが小さいので、vmin が幅基準・vmax が高さ基準に入れ替わります。端末の向きが変わっても基準が自動で切り替わるのが、この2つの単位の特徴です。
% との違いは「何を基準にするか」
ビューポート単位と混同されやすいのが % です。見た目はどちらも「画面に合わせて伸び縮みするサイズ指定」ですが、基準がまったく違います。% は親要素が基準、ビューポート単位はビューポートが基準です。
width: 50% は「親要素の幅の半分」なので、親が 600px なら 300px、親が 400px なら 200px と、親の大きさに引きずられます。一方 width: 50vw は「ビューポート幅の半分」であり、どんなに狭い親の中に入れても画面幅の半分の大きさになります。その結果、親からはみ出すことも珍しくありません。
下のデモで違いを確かめてみてください。青い枠が親要素(幅 50%)で、その中の白い箱は width: 80%、黄色い箱は width: 80vw です。同じ「80」でも、黄色い箱だけが親を大きくはみ出しています。CSS タブで .box--vw の 80vw を 30vw に変えると、親の中に収まるようになります。
<div class="parent">
<p class="label">この枠が親要素(幅 50%)です</p>
<div class="box box--percent">width: 80% → 親要素の幅の80%</div>
<div class="box box--vw">width: 80vw → ビューポート幅の80%(親をはみ出します)</div>
</div>
<div class="box box--vh">height: 20vh → ビューポートの高さの20%</div>
body {
margin: 0;
padding: 16px;
font-family: sans-serif;
background: #f8fafc;
color: #1e293b;
font-size: 13px;
}
/* 親要素の幅は、このプレビュー画面(ビューポート)の半分 */
.parent {
width: 50%;
padding: 12px;
background: #e0e7ff;
border: 1px solid #a5b4fc;
border-radius: 8px;
}
.label {
margin: 0 0 10px;
font-weight: bold;
color: #3730a3;
}
.box {
padding: 10px;
margin-bottom: 10px;
background: #fff;
border: 1px solid #6366f1;
border-radius: 4px;
box-sizing: border-box;
}
/* % は「親要素」が基準。親が縮めば一緒に縮む */
.box--percent {
width: 80%;
}
/* vw は「ビューポート」が基準。親の幅とは無関係なので、はみ出す */
.box--vw {
width: 80vw;
background: #fef3c7;
border-color: #f59e0b;
}
/* vh はビューポートの高さが基準。高さだけ指定して幅は自動 */
.box--vh {
height: 20vh;
margin-top: 16px;
background: #dcfce7;
border-color: #22c55e;
}
高さについても同じことが言えます。height: 100% は「親要素の高さの100%」なので、親に高さが決まっていないと計算できず効きません。<html> から順に height: 100% を書き足していく、という古くからよく知られた面倒があるのはこのためです。height: 100vh なら親の高さに関係なくビューポートの高さが直接手に入るので、この手間が要らなくなります。
ビューポート単位が活きる場面
ビューポート単位はどこにでも使えばよいものではなく、「画面そのものの大きさに意味がある」場面で効果を発揮します。代表的な使いどころを見ていきます。
画面いっぱいのヒーローセクション
もっとも定番の用途が、トップページの最初に置く全画面のセクションです。height: 100vh(後述する 100dvh を使うとさらに確実です)を指定すれば、コンテンツの量に関係なく画面の高さいっぱいになり、「まず1画面ぶんの印象を見せて、続きはスクロールで読ませる」という構成が作れます。中身の縦位置を中央に揃えたいときは Flexbox と組み合わせます。
.hero {
height: 100vh;
display: flex;
align-items: center; /* 縦方向の中央揃え */
justify-content: center; /* 横方向の中央揃え */
}
なお height ではなく min-height: 100vh にしておくと、中身が画面より多くなったときに切れずに伸びてくれます。ヒーローセクションのように中身が確実に収まる場合は height、可変のコンテンツを入れる場合は min-height と使い分けるのがおすすめです。
画面幅に追従する文字サイズ
見出しの font-size に vw を使うと、画面が広いほど文字が大きくなります。ただし font-size: 5vw のように単独で使うと、狭い画面では読めないほど小さくなり、大きなモニターでは巨大になりすぎます。そこで下限と上限を付けられる clamp() と組み合わせるのが定番です。clamp(最小値, 可変値, 最大値) と書けば、可変値の部分に vw を使いつつ、はみ出さない範囲に収められます。
.page-title {
/* 4vw を基準にしつつ、24px より小さく・56px より大きくしない */
font-size: clamp(24px, 4vw, 56px);
}
.section {
/* 余白も画面幅に応じて広げる */
padding-block: clamp(40px, 8vw, 120px);
}
同じ考え方は余白にも使えます。セクションの上下 padding を vw ベースにしておくと、スマートフォンでは詰まり、大きな画面ではゆったりと空くようになり、ブレイクポイントごとに値を書き分ける手間が減ります。
向きが変わっても崩れない正方形
vmin が役立つのは、画面のどこかに必ず収めたい正方形の要素です。width: 80vw; height: 80vw; と書いた正方形は、横向きの画面では高さが足りずに画面からはみ出します。これを width: 80vmin; height: 80vmin; にすると、常に短いほうの辺を基準にするので、縦向きでも横向きでも画面内に収まります。全画面表示のゲーム盤やモーダル内の画像など、縦横比を保ったまま確実に収めたい要素に向いています。
反対に vmax は、画面の長いほうの辺を基準にしたいときに使います。背景を必ず覆い切りたい装飾要素などで、どちらの向きでも足りるサイズを一発で指定できます。使用頻度は vmin より低めですが、覚えておくと選択肢が広がります。
100vw を指定すると横スクロールが出る理由
ビューポート単位で最初に踏む地雷が、width: 100vw による横スクロールです。指定した要素の幅は画面と同じはずなのに、ページの下に横スクロールバーが出て、数ピクセルだけ右に動かせてしまう。これはバグではなく仕様どおりの結果です。
縦スクロールバーの幅が含まれている
原因は、ビューポート単位がスクロールバーの存在を無視して計算されることにあります。CSS の仕様では、ルート要素の overflow が auto のとき、ビューポート単位はスクロールバーが無いものとして扱われます。つまり 100vw は「スクロールバーの領域も含めた幅」です。
ところが、ページが縦に長ければ Windows などの環境では縦スクロールバーが表示され、その幅(おおむね 15px 前後)だけコンテンツを置ける領域は狭くなります。結果として 100vw の要素は実際の表示領域よりわずかに広くなり、はみ出したぶんが横スクロールとして現れる、という仕組みです。スクロールバーが画面に重なって表示される macOS や多くのスマートフォンでは起きないため、開発中に気づきにくいのも厄介なところです。
そもそも 100vw を使わない
もっとも確実な対処は、幅いっぱいの指定に 100vw を使わないことです。<div> や <section> のようなブロック要素は、幅を指定しなければ自動的に親の幅いっぱいに広がります。ページ全体を覆う要素なら、幅の指定そのものを消すか width: 100% にすれば、スクロールバーを除いた正しい幅になります。
/* 横スクロールが出やすい書き方 */
.full-width {
width: 100vw;
}
/* ブロック要素なら、幅を指定しないだけで親の幅いっぱいになる */
.full-width {
width: auto;
}
幅が制限されたコンテナの中から、背景だけを画面端まで広げたい場合もあります。負のマージンと 100vw を組み合わせる手法が知られていますが、これも同じスクロールバー問題を抱えます。どうしても回避できないときは body に overflow-x: hidden を指定してはみ出しを隠す方法がありますが、これはスクロールの起点となる要素が変わるため、position: sticky が効かなくなるなどの副作用があります。使う場合は、他の箇所の挙動が変わっていないか必ず確認してください。
スマホで 100vh が画面からはみ出す問題
もうひとつの有名な問題が、スマートフォンでの 100vh です。画面いっぱいのつもりで作ったセクションが、iPhone や Android で見ると下側がアドレスバーに隠れ、少しだけスクロールできてしまう。長年フロントエンド開発者を悩ませてきた現象で、これを解決するために追加されたのが動的ビューポート単位です。
アドレスバーの出入りでビューポートの高さが変わる
スマートフォンのブラウザは、ページを下にスクロールすると上のアドレスバーや下のツールバーを隠し、表示領域を広げます。上に戻すと再びバーが現れて領域が狭くなります。つまりビューポートの高さは操作に応じて刻々と変わるのです。
ところが vh はこの変化に追従しません。多くのモバイルブラウザは、スクロール中にレイアウトが揺れ続けるのを避けるため、バーが隠れた状態の大きいほうの高さを vh の基準として固定しています。そのため、バーが表示されている状態では 100vh の要素が実際の表示領域より高くなり、下がはみ出して見えるというわけです。
svh・lvh・dvh の違い
この「状態によって高さが変わる」問題に対応するため、CSS には3種類のビューポートが定義され、それぞれに対応する単位が用意されました。頭に付く s / l / d は small(小)・large(大)・dynamic(動的)の頭文字です。
| 単位 | 基準になる高さ | 値の変化 |
|---|---|---|
svh | ブラウザの UI がすべて表示されているときの、もっとも小さいビューポートの高さの1% | 変わらない |
lvh | ブラウザの UI が隠れているときの、もっとも大きいビューポートの高さの1% | 変わらない |
dvh | 現在のビューポートの高さの1% | UI の出入りに合わせて変わる |
vh | UA が既定とするビューポートの高さの1%。多くのモバイルブラウザでは lvh と同じ値になる | 変わらない |
100svh はアドレスバーが出ている状態の高さなので、どの状態でも絶対に画面からはみ出しません。代わりに、バーが隠れているときは下に余りができます。100lvh はその逆で、バーが隠れているときはぴったり収まりますが、出ているときははみ出します。従来の 100vh で起きていた問題は、実質的に 100lvh と同じ挙動だったと考えると分かりやすいです。
どれを選べばよいか
基本的には dvh を選べば、常に現在の表示領域とぴったり一致します。ヒーローセクションのように「いま見えている画面を埋めたい」用途では、これがもっとも自然な結果になります。
ただし dvh は値が変化する単位なので、アドレスバーが出入りするたびに要素の高さが変わり、レイアウトがガタつくことがあります。画面下部に固定したボタンなど、絶対に隠れてはいけない要素を含む領域では、高さが変わらず必ず収まる svh のほうが安全です。一方 lvh は「最大の高さで固定したい」という限定的な場面で使うもので、日常的に使う機会は多くありません。
下のデモは height: 100dvh を指定したヒーローセクションです。プレビュー画面の高さいっぱいに広がり、その下に次のセクションが続きます。CSS タブで 100dvh を 50dvh に変えると、高さが半分になって次のセクションが顔を出します。実機のスマートフォンで 100vh と 100dvh を見比べると、違いがはっきり分かります。
<section class="hero">
<div class="hero__inner">
<p class="hero__title">ヒーローセクション</p>
<p class="hero__text">height: 100dvh でビューポートの高さいっぱいに広がります</p>
</div>
</section>
<section class="next">
<p>次のセクションです。スクロールすると出てきます。</p>
</section>
body {
margin: 0;
font-family: sans-serif;
color: #1e293b;
}
.hero {
/* 先に vh を書き、後から dvh で上書きする。
dvh を知らない古いブラウザは vh のほうを使う */
height: 100vh;
height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, #6366f1, #22d3ee);
color: #fff;
}
.hero__inner {
padding: 16px;
}
/* 4vw を基準にしつつ、下限 18px・上限 32px で頭打ちにする */
.hero__title {
margin: 0 0 8px;
font-size: clamp(18px, 4vw, 32px);
font-weight: bold;
}
.hero__text {
margin: 0;
font-size: 13px;
opacity: 0.9;
}
.next {
padding: 24px 16px;
background: #f8fafc;
font-size: 13px;
}
.next p {
margin: 0;
}
古いブラウザへのフォールバック
動的ビューポート単位は主要なブラウザで利用できますが、古いバージョンには届きません。対応していないブラウザは height: 100dvh という宣言そのものを無視するので、先に vh の指定を書き、あとから dvh で上書きするだけでフォールバックになります。CSS は後に書いたほうが優先され、解釈できない宣言は捨てられるという性質をそのまま利用した書き方です。
.hero {
height: 100vh; /* dvh を知らないブラウザはこちらを使う */
height: 100dvh; /* 対応ブラウザはこちらで上書きされる */
}
幅方向の svw・lvw・dvw
s / l / d の接頭辞は高さ専用ではありません。幅にも svw / lvw / dvw があり、vmin / vmax にも svmin・lvmax・dvmin といった組み合わせがすべて用意されています。考え方はまったく同じで、小さいビューポート・大きいビューポート・現在のビューポートのどれを基準にするかを選ぶだけです。
ただし実務での出番は高さほど多くありません。モバイルブラウザのアドレスバーやツールバーは画面の上下に配置されるため、ビューポートの幅は出入りしても変わらないことがほとんどだからです。そのため多くの環境で svw・lvw・dvw は vw と同じ値になります。「同じ命名規則で幅方向も揃っている」と知っておけば十分で、通常は vw を使えば問題ありません。
まとめ
ビューポート単位は、親要素ではなく画面そのものを基準にする長さの単位です。vw は幅の1%、vh は高さの1%、vmin と vmax はそれぞれ短いほう・長いほうの辺の1%を表します。% が親要素基準であるのに対し、ビューポート単位は親の大きさに影響されないため、画面いっぱいのセクションや画面幅に追従する文字サイズ・余白の指定に向いています。
注意したいのは2つの落とし穴です。100vw は縦スクロールバーの幅を含むため横スクロールを生みやすく、幅いっぱいの指定にはそもそも使わないほうが安全です。そして 100vh はスマートフォンのアドレスバーの出入りに追従せず、画面からはみ出します。こちらは dvh(現在の高さ)・svh(もっとも小さい高さ)・lvh(もっとも大きい高さ)で解決できます。画面を埋めたいなら dvh、必ず収めたいなら svh を選び、height: 100vh; を先に書いてフォールバックさせておけば、古いブラウザまで含めて安心して使えます。