JavaScript + CSS: ヘッダを上部に固定してカラムの中身はスクロールさせる

お題は、つぎのサンプル001のようなレイアウトです。ヘッダを上部に固定し、左カラムのリストはスクロールします。

#### サンプル001■JavaScript + CSS: Fixed top header and scrolling column
<p class="codepen" data-height="360" data-theme-id="light" data-default-tab="html,result" data-user="FumioNonaka" data-slug-hash="OJMwJxr" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="JavaScript + CSS: Fixed top header and scrolling column">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/OJMwJxr">
  JavaScript + CSS: Fixed top header and scrolling column</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


たとえば、Bootstrapの「[Documentation](https://getbootstrap.com/docs/4.5/components/alerts/)」もこのスタイルを採り入れています。

#### 図001■BootstrapのDocumentationページ
![2007001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/89676b4e-1e2e-ff08-d64c-47a04bcb9e3d.png)


基本となるCSSの設定はごく簡単です。けれど、ユーザーがウィンドウサイズを変えたときなど、設定は動的に更新しなければなりません。この部分は、JavaScriptコードで処理します。


# ページの基本的な構成
サンプルの`<body>`要素の中身はつぎのように組み立てました。ページを構成する要素は大きく3つ、ヘッダと左カラム、そしてメインコンテンツです(図002)。CSSの`class`属性の定めは省いています。確かめたい方は、前掲サンプル001をご覧ください(Bootstrap 4.5使用)。

```html:&lt;body&gt;要素
<header id="header">
	<h1>Header</h1>
</header>
<div>
	<div id="left-column">
		<h3>Left column</h3>
		<ul id="list">
			<li>...[中略]...</li>
		</ul>
	</div>
	<main>
		<h2>Main contents</h2>
		<p>...[中略]...</p>
	</main>
</div>
```

#### 図002■ページはヘッダと左カラムにメインコンテンツで組み立てられる
![2007001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/aa823315-48ee-c9a1-c663-46bb6d09e5e6.png)


# ヘッダをページ上部に固定する
まずはサイトでよく見かける、ヘッダあるいはトップナビゲーションをページ上部に固定するCSSの設定です。

位置は[`position`](https://developer.mozilla.org/docs/Web/CSS/position)プロパティに`fixed`を与えれば固定できます。具体的な置き場所は、別にプロパティで定めなければなりません。上部なら`top: 0`です。ただし、`<body>`要素の領域に含まれなくなることにお気をつけください。そのままでは、ページの上部がかぶって隠れてしまうのです(図003)。

```css:&lt;style&gt;要素
#header {
	position: fixed;
	top: 0;
}
```

#### 図003■ページ上部にヘッダがかぶって見えない
![2007001_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/3b5c75d1-aab2-8d52-a5ca-98ae78e50c3b.png)

`<body>`要素の`padding-top`は、ヘッダの高さ分下げなければなりません。サンプルでは、ヘッダの高さを調べると72pxでした。

```css
body {
	padding-top: 72px;
}
```

これで、ヘッダはページ上部に固定されます。


# カラムの中身をスクロールさせる
要素の中のテキストをスクロールさせるには、その高さ(`height `)が具体的に決められていなければなりません。そのうえで、[`overflow`](https://developer.mozilla.org/docs/Web/CSS/overflow)プロパティは`scroll`にします。高さに収まらないテキストは、スクロールして表示する設定です(図004)。

```css
#left-column {
	height: 500px;
	overflow: scroll;
}
```

#### 図004■左カラムの高さに収まらないテキストはスクロールで表示される
![2007001_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/f98764ae-38a8-cbc8-0c07-10c83bd5d072.png)

もっとも、高さを決め打ちしたのでは、役に立ちません。ユーザーが開いたウィンドウサイズに合わせて、カラムの高さをJavaScriptで変えましょう。

```css:&lt;styel&gt;要素
#left-column {
	/* height: 500px; */

}
```


# カラムの高さを動的に決める
ブラウザウィンドウの内側([ビューポート](https://developer.mozilla.org/docs/Web/CSS/Viewport_concepts))の高さを返すのは[`window.innerHeight`](https://developer.mozilla.org/docs/Web/API/Window/innerHeight)プロパティです。また、要素の高さは[`element.clientHeight`](https://developer.mozilla.org/docs/Web/API/Element/clientHeight)で得られます。

したがって、ビューポートの高さからヘッダの高さを差し引いて、左カラムの高さとすればよさそうです。ただし、`element.clientHeight`は、読み取り専用プロパティであることにご注意ください。そのため、高さの設定には、[`HTMLElement.style`](https://developer.mozilla.org/docs/Web/API/HTMLElement/style)を用います。プロパティ(`height`)の値は、要素のインライン`style`属性と同じ文字列です。単位(`px`)も忘れないようにしてください。

```js:&lt;script&gt;要素
document.addEventListener('DOMContentLoaded', () => {
	const header = document.getElementById('header');
	const leftColumn = document.getElementById('left-column');
	const setLayout = () =>
	leftColumn.style.height = (window.innerHeight - header.clientHeight) + 'px';
	setLayout();
	window.addEventListener('resize', setLayout);
}
```


# スクリプトを改善する
ヘッダの高さはJavaScriptコードでわかったのですから、`<body>`要素の`padding-top`もこの処理の中で定めましょう。ヘッダやナビゲーションバーの高さは、その中身によってはウィンドウ幅に応じて変わることもあるからです。

```css:&lt;style&gt;要素
body {
	/* padding-top: 72px; */

}
```

`HTMLElement.style`に定めるのはJavaScriptのプロパティです。つまり、ケバブケース(`padding-top`)は使えないことにお気をつけください。キャメルケース(`paddingTop`)に改めるのが決まりです。

```js:&lt;script&gt;要素
document.addEventListener('DOMContentLoaded', () => {
	const body = document.querySelector('body');
	const setLayout = () => {
		const headerHeight = header.clientHeight;
		body.style.paddingTop = headerHeight + 'px';
		leftColumn.style.height = (window.innerHeight - headerHeight) + 'px';
	}

}
```

スクロール操作を試していると、ちょっとした不具合が見つかります。メインの領域のテキストが多い場合、下までスクロールすると左カラムも上に動いてしまうのです(図005)。

#### 図005■メイン領域とともに左カラムが上に動いてしまう
![2007001_005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/c8223170-f958-7b0e-2d19-c0e5415a5f09.png)

動かなくするには、`<body>`要素と同じく左カラムも固定しなければなりません。すると、カラムが`<body>`の領域から外れて、メイン(`<main>`要素)のテキストが左に潜ってしまいます。左余白の調整も必要です(`margin`を使ったのはCSSですでに`padding`を定めていたからです)。

```js:&lt;script&gt;要素
document.addEventListener('DOMContentLoaded', () => {

	const main = document.querySelector('main');

	const setLayout = () => {

		leftColumn.style.position = 'fixed';
		leftColumn.style.top = headerHeight;
		main.style.marginLeft = leftColumn.clientWidth + 'px';
	}

}
```

CSSの定めと書き上げたJavaScriptの記述は、つぎのコード001のとおりです。細かい内容や動きは、冒頭のサンプル001でお確かめください。

#### コード001■&lt;header&gt;を上部に固定して&lt;div&gt;の中身はスクロールさせる
```css:&lt;style&gt;要素
#header {
	position: fixed;
	top: 0;
}
#left-column {
	overflow: scroll;
}
```

```js:&lt;script&gt;要素
document.addEventListener('DOMContentLoaded', () => {
	const body = document.querySelector('body');
	const main = document.querySelector('main');
	const header = document.getElementById('header');
	const leftColumn = document.getElementById('left-column');
	const setLayout = () => {
		const headerHeight = header.clientHeight;
		body.style.paddingTop = headerHeight + 'px';
		leftColumn.style.height = (window.innerHeight - headerHeight) + 'px';
		leftColumn.style.position = 'fixed';
		leftColumn.style.top = headerHeight;
		main.style.marginLeft = leftColumn.clientWidth + 'px';
	}
	setLayout();
	window.addEventListener('resize', setLayout);
});
```