CSSのposition: stickyでテーブルのヘッダー行・列を固定する

CSSの `position: sticky` を使ってテーブルのヘッダー行・列を固定する方法を解説します。動作確認したブラウザーは次のとおりです。

* Google Chrome 71
* Firefox 64
* Safari 12
* Microsoft Edge 43 (EdgeHTML 17)

ちなみに、IE 11などの対応していないブラウザーで見た場合、ヘッダー行・列が固定されないだけで、表示が崩れたりはしません。

## theadを固定するかthを固定するか

縦スクロール時にヘッダー行を固定するにあたり、`thead` に対して `position: sticky` を指定したくなりますが、これはあまり筋が良くなさそうです。まずChromeやEdgeは `thead` や `tr` の固定に対応していません[^1]。 `display: block` などで回避できますが、列の幅が決まっていないといけないという制約が生まれたり、本質的でないスタイルの記述が増えたりします[^2]。

一方 `th` を固定すると、記述がシンプルになり、列の幅も予め決めなくて済みます。このため、**ChromeやEdgeが `thead` の固定に対応していない現状では、`thead` を固定するのではなく、 `thead` 内の `th` を固定するのが無難です[^3]。** ただし複数のヘッダー行を固定したい場合はちょっと工夫が必要なので後述します。

[^1]: Chrome: https://bugs.chromium.org/p/chromium/issues/detail?id=702927 Edge: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/16765952/
[^2]: 参考: https://qiita.com/s0tter/items/14fb4ec2600828a21a22 個人的に別のアプローチを試したところ、SafariやEdgeで左上隅のセルが正しくレンダリングされませんでした（しかもSafariの場合 `iframe` 内では発生しないみたいでわかりづらい）。  https://codepen.io/orangain/pen/zyMzga
[^3]: 将来的にChromeやEdgeでも `thead` を固定できるようになればそのほうが簡単かもしれませんが、Issueを見る限り時間がかかりそうです。

## ヘッダーをビューポートの上と左に固定する

まずはビューポート（ウィンドウ）の上と左に固定します。

![スクリーンキャスト](https://qiita-image-store.s3.amazonaws.com/0/4240/ada7bc5b-6c71-4cbe-d5a2-b151926d24ea.gif)

デモ: [StickyTable (thをビューポートに固定)](https://codepen.io/orangain/pen/REqjjv)

以下のような記述で簡単に固定できます。デザインのためのCSSは省略しています。

### HTML

1行目と1列目がヘッダーというよくあるテーブルです。

```html
<table class="sticky_table">
  <thead>
    <tr>
      <th></th>
      <th>head</th>
      <th>head</th>
      <!-- ... -->
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>left</th>
      <td>data</td>
      <td>data</td>
      <!-- ... -->
    </tr>
    <!-- ... -->
  </tbody>
</table>
```

### CSS

縦スクロール時にヘッダー行を固定します。

```css
.sticky_table thead th {
  /* 縦スクロール時に固定する */
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  /* tbody内のセルより手前に表示する */
  z-index: 1;
}
```

横スクロール時にヘッダー列を固定します。

```css
.sticky_table th:first-child {
  /* 横スクロール時に固定する */
  position: -webkit-sticky;
  position: sticky;
  left: 0;
}
```

横スクロール時に左上隅のセルが他のセルより手前に来るようにします。

```css
.sticky_table thead th:first-child {
  /* ヘッダー行内の他のセルより手前に表示する */
  z-index: 2;
}
```

## バリエーション1: ヘッダーをoverflow: scrollな要素の上と左に固定する

`position: sticky` を指定した要素は、一番近いscrolling ancestorに固定されます。scrolling ancestorとは、祖先要素のうち `overflow: hidden, scroll, auto, overlay` な要素です。そのような要素が無い場合はビューポートと考えて良いでしょう。

先ほどのテーブルを `overflow: scroll` を指定した要素で囲めば、その要素内でスクロールしたときにヘッダーが上と左に固定されます。

デモ: [StickyTable (thをoverflow: scrollな要素に固定)](https://codepen.io/orangain/pen/dwQZra)

### HTML

テーブルをスクロールする要素で囲います。

```html
<div class="sticky_table_wrapper">
  <table class="sticky_table">
    <!-- 上記と同様 -->
  </table>
</div>
```

### CSS

上記のCSSに加えて、次のCSSを書きます。 `width` や `height` は好みで設定して下さい。

```css
.sticky_table_wrapper {
  overflow: scroll;
  width: calc(100vw - 1rem);
  height: 75vh;
}
```


## バリエーション2: 複数のヘッダー行を固定したい場合

`th` を固定する方針では、複数のヘッダー行があるときに工夫が必要です。ヘッダー行の高さを予め決めるという制約を導入すれば、各ヘッダー行の `top` の値を変えてやることで意図した場所に固定できます。

デモ: [StickyTable (ヘッダー行が複数の場合にthをビューポートに固定)](https://codepen.io/orangain/pen/yGQzpd)

例えば `thead` 内のヘッダー行が3行ある場合、縦スクロール時に固定するにはスタイルを次のように変更します。

```css
.sticky_table thead th {
  /* 縦スクロール時に固定する */
  position: -webkit-sticky;
  position: sticky;
  /* 高さが変わらないよう改行させない */
  white-space: nowrap;
}
.sticky_table thead tr:nth-child(1) th {
  top: 0;
}
.sticky_table thead tr:nth-child(2) th {
  top: 1.5rem; /* 2行目は1行目の高さの位置に固定する */
}
.sticky_table thead tr:nth-child(3) th {
  top: 3rem; /* 3行目は1〜2行目の高さの位置に固定する */
}
```


## 表のボーダーについて

この記事では深入りしませんが、表にボーダーがあると固定されたときに微妙にセルの位置がずれて見えたり、一部のボーダーがレンダリングされないことがあります。気になる場合は、次の2つの回避策のいずれかが良いかと思います。

* 表にボーダーを使用しない
* `border-collapse: separate` を使う

## 参考

* [position | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/position)
* [Can I use... Support tables for HTML5, CSS3, etc](https://caniuse.com/#feat=css-sticky)
* [CSSのみで行・列ヘッダ固定テーブルを実装（Firefox / Chrome / Safari） - Qiita](https://qiita.com/s0tter/items/14fb4ec2600828a21a22) (※theadを固定する方針。CodePenをForkさせていただきました。感謝！)
* [CSSスニペット：position: sticky;でtableを下スクロール時はヘッダ、横スクロール時は最左を固定 | かちびと.net](http://kachibito.net/css/a-simple-responsive-spreadsheet)
* [Firefox/Chromeでtable要素の「position: sticky」が動作しない問題の対処: 小粋空間](http://www.koikikukan.com/archives/2017/05/23-000300.php) (※theadを固定する方針)
* [position: stickyの面白い使い方と使用時の注意点 | Rriver](https://parashuto.com/rriver/development/position-sticky)
* [table に position: sticky を使ってみた | WEBMAN](https://webman-japan.com/webcreate/used-table-position-sticky/)
* [CSSのposition:stickyがすごく便利 | q-Az](https://q-az.net/position-sticky/)
* [\[CSS\] z-index とスタックコンテキスト - Qiita](https://qiita.com/hoto17296/items/42e62989193504d512c7)