症状:サイドバーと本文のスクロールが安定しない
管理画面を作るとき、サイドバーへheight: 100%を指定しても本文と高さが揃わない、本文をスクロールするとサイドバーまで流れてしまう、画面の右端と本文内にスクロールバーが2本出る、といった問題が起きます。
このレイアウトでは、サイドバーへposition: fixedやposition: stickyを足す前に、画面全体の高さとスクロールを担当する要素を決めることが重要です。この記事では、ヘッダーの下にサイドバーと本文が並ぶ管理画面を題材に、CSSだけで直します。
原因は次の3つ
原因1:height: 100%の基準になる高さがない
パーセントの高さは親要素の高さを基準にします。親の高さが内容任せのautoなら、子だけにheight: 100%を書いても画面の残り高さにはなりません。
原因2:GridやFlexの子が内容より小さくならない
GridやFlexの子には自動最小サイズがあります。長い本文を持つ領域が縮まらず、外側を押し広げる場合はminmax(0, 1fr)やmin-height: 0が必要です。
原因3:複数の要素がスクロールを担当している
body、画面全体の枠、本文へ重ねてoverflow: autoを指定すると、スクロールバーが二重になります。外枠は固定し、実際に長くなる領域だけをスクロールさせます。
失敗版と修正版の比較
デスクトップ幅では、各デモの本文をスクロールして違いを確認できます。失敗版は外枠にも本文にもスクロールバーがあり、外枠を動かすとヘッダーやサイドバーの位置までずれます。修正版は外枠を固定し、サイドバーと本文だけが必要に応じて独立してスクロールします。
画面幅が狭い場合は、デモも実装例も縦並びへ切り替わります。モバイルではサイドバーを固定せず、ページ全体を自然にスクロールさせる設計です。
修正版のHTMLとCSS
次のHTMLとCSSは、そのまま最小構成として使えます。JavaScriptは不要です。外枠で画面高を決め、Gridの2行目へ「残りの高さ」を割り当てます。
<div class="app-shell">
<header class="app-header">
<strong>業務管理</strong>
<span>田中さん</span>
</header>
<div class="app-body">
<aside class="app-sidebar">
<nav aria-label="メインメニュー">
<a href="#" aria-current="page">ダッシュボード</a>
<a href="#">受注管理</a>
<a href="#">顧客管理</a>
</nav>
</aside>
<main class="app-main">
<h1>ダッシュボード</h1>
<!-- 長い本文 -->
</main>
</div>
</div>
html,
body {
margin: 0;
}
.app-shell {
/* 100vhは古いブラウザ向けのフォールバック */
height: 100vh;
height: 100dvh;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 64px;
padding: 0 24px;
}
.app-body {
min-height: 0;
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
}
.app-sidebar,
.app-main {
min-height: 0;
overflow: auto;
}
@media (max-width: 768px) {
.app-shell {
height: auto;
min-height: 100dvh;
overflow: visible;
}
.app-body {
display: block;
}
.app-sidebar,
.app-main {
overflow: visible;
}
}
100dvhは、表示中のブラウザUIを考慮した動的なビューポート高です。先に100vhを書いてから上書きすると、dvhを理解しない古いブラウザでも画面高を確保できます。
原因1:height: 100%の基準がない
height: 100%は「画面の残り全部」という意味ではありません。親に確定した高さがある場合に、その高さと同じ値になります。ヘッダーの下に置いた要素へ指定すると、ヘッダー分を差し引かず親と同じ高さになり、合計が画面より大きくなることもあります。
/* 失敗:headerの高さを引かず、bodyだけで親と同じ高さになる */
.app-body {
height: 100%;
}
/* 修正:Gridへ残りの高さを計算させる */
.app-shell {
height: 100dvh;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
ヘッダー高をcalc(100vh - 64px)で引く方法もありますが、ヘッダーが折り返したり高さが変わったりすると値がずれます。1行目をautoにしておけば、実際のヘッダー高をブラウザが計算します。
原因2:GridやFlexの子が縮まない
1frは残りの空間を受け取りますが、Gridトラックの最小値は既定でautoです。中身が長い場合、その内容に必要な高さより小さくならず、外枠を押し広げることがあります。そこで行をminmax(0, 1fr)にし、子要素にもmin-height: 0を指定します。
.app-shell {
/* 1frだけにせず、最小値を0にする */
grid-template-rows: auto minmax(0, 1fr);
}
.app-body {
min-height: 0;
}
/* Flexboxで縦に組む場合も同じ */
.app-body {
min-height: 0;
flex: 1;
}
DevToolsでは、外枠の高さと.app-bodyの高さを比べます。子のほうが大きく、min-heightの計算値がautoなら、この原因を疑えます。
原因3:overflowの指定先が多い
スクロールさせたいからと、外枠、左右のレイアウト枠、本文のすべてへoverflow: autoを付けると、複数のスクロール領域が生まれます。スクロールを担当するのは、内容が増えるサイドバーと本文だけです。
| 要素 | overflow | 役割 |
|---|---|---|
.app-shell | hidden | 画面外へはみ出させない |
.app-body | 指定なし | 左右の領域を配置する |
.app-sidebar | auto | メニューが長い場合だけスクロールする |
.app-main | auto | 本文をスクロールする |
キーボードでもスクロール領域へ移動できるよう、必要に応じてtabindex="0"を付け、領域の目的をaria-labelで伝えます。スクロールバーをCSSで消すと現在位置や操作対象が分かりにくくなるため、常時非表示にはしません。
stickyを使うレイアウトとの違い
この記事の構成は、画面高を固定した管理画面で、本文とサイドバーを別々にスクロールさせる方法です。ブログや商品ページのようにbody全体をスクロールし、サイドバーだけを途中から追従させたい場合はposition: stickyを使います。
stickyが親のoverflowや高さの影響で動かない場合は、「position: stickyが効かない4つの原因と直し方」で切り分けてください。管理画面の独立スクロールと、文書全体のstickyは混ぜず、どちらか一方をスクロール設計の基準にします。
一次情報の確認先
パーセント指定の高さは包含ブロックを基準にすることをMDNのheightで、Gridアイテムの自動最小サイズとminmax()の定義をCSS Grid Layout Module Level 2で確認できます。dvhを含む動的ビューポート単位はCSS Values and Units Level 4に定義されています。
まとめ
サイドバーの高さとスクロールを安定させるには、外枠を100dvhで画面高に固定し、ヘッダー以外の行をminmax(0, 1fr)にします。そのうえで子へmin-height: 0を指定し、overflow: autoはサイドバーと本文だけに持たせます。
サイドバー自体のメニュー実装は「サイドバーナビゲーション — ツリー折りたたみ型」と「サイドバーナビゲーション — アイコン折りたたみ型」、複数領域が連動する画面構成は「3ペインレイアウト」で確認できます。