サイドバーの高さが揃わない・スクロールで固定されない3つの原因と直し方

症状:サイドバーと本文のスクロールが安定しない

管理画面を作るとき、サイドバーへheight: 100%を指定しても本文と高さが揃わない、本文をスクロールするとサイドバーまで流れてしまう、画面の右端と本文内にスクロールバーが2本出る、といった問題が起きます。

このレイアウトでは、サイドバーへposition: fixedposition: 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を付けると、複数のスクロール領域が生まれます。スクロールを担当するのは、内容が増えるサイドバーと本文だけです。

キーボードでもスクロール領域へ移動できるよう、必要に応じて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ペインレイアウト」で確認できます。