スクローラブルなコンテナ内で fixed 的配置 (by position: sticky)

# :congratulations: Chrome `position: sticky` 復活記念 :tada:

先日リリースされた [Chrome 56 で `position: sticky` が戻ってきた](https://developers.google.com/web/updates/2016/12/position-sticky) のを記念して、`position: sticky` を活用した小技です。

<ins>2017/02/16: 前稿におけるネガティブマージンの指定に問題があったため、記事を修正しました。</ins>

# やりたいこと

`overflow: auto` や `overflow: scroll` などで **スクロール可能にしたコンテナ要素の中で、`position: fixed` のように自由に固定配置をしたい。**

例として、以下のような縦横にスクロール可能なコンテナ要素を用意します。

![scrollable-container.png](https://qiita-image-store.s3.amazonaws.com/0/70467/e6abf54b-f78f-2ab9-f00b-a564e3f0961c.png)

```html:HTML
<div class="scrollable-container">
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
  <p>...</p>
  <p>...</p>
</div>
```

```scss:CSS&nbsp;(SCSSで書いてます)
.scrollable-container {
  overflow: scroll;
  border: 1px solid silver;

  width: 10em;
  height: 10em;
  
  p { width: 20em; }
}
```

**『このコンテナのど真ん中に、固定配置で ":star:" を置く』** のをゴールにしてみます。

## 失敗例

### `position: fixed`

`position: fixed` はビューポート（≒`<body>`）に対する固定配置になるので、コンテナ要素の中で使っても、外に追い出されてしまいます。

![position-fixed.png](https://qiita-image-store.s3.amazonaws.com/0/70467/dec0e5b0-4554-fbad-7c32-60628f2399bc.png)

### `position: absolute`

だからといって `position: absolute` は、スクロールにくっついてしまうので、固定配置とは言えません。ちょっと右に動かしたらご覧の通り。

![position-absolute.png](https://qiita-image-store.s3.amazonaws.com/0/70467/6d892111-4c6f-4628-7b21-6ae1d93dacc5.png)

そこで `position: sticky` の出番です。

# `position: sticky` とは

普段はそのまま表示されますが、要素の位置がスクロールなどによって `top` `left` などで指定したしきい値を超えると、その位置で絶対配置で固定されるようになる要素です。

[詳しい説明が MDN にあります](https://developer.mozilla.org/ja/docs/Web/CSS/position#Sticky_positioning)が、正直、言葉じゃちんぷんかんなので、[デモページなどを見ると感じが掴めるのではないかと思います。](http://html5-demos.appspot.com/static/css/sticky.html)

もし iOS ユーザーなら、連絡帳アプリなどで、スクロールしても頭文字がずっとヘッダに固定される挙動を見たことがあると思いますが、アレが CSS で実現できると思ってください。(雑)

## 対応状況

[Can I use で見てみてください。](http://caniuse.com/#search=sticky) Firefox はとうの昔から対応していましたが、Chrome は experimental な実装のまま、一度ドロップした経緯があります。52 で再度 experimental で実装され、先日の 56 のリリースで晴れて正式な実装となりました。(参考: [Chrome 56 に position: sticky; が戻ってきた](https://hyper-text.org/archives/2017/01/chrome_56_position_sticky.shtml))

WebKit では 6.1 (iOS 6.1) から使えていますが、接頭辞 `-webkit-sticky` が必要です。IE, Edge は目を瞑りましょう。

# アプローチ

基本的な要領は、`position: sticky` でよく見るヘッダ固定と同じなんですが、

- `position: sticky` で固定する要素の大きさを、**コンテナ領域全体** に拡大
- その要素の中で `position: absolute` を使って絶対配置する

といった具合です。

`position: fixed` は "固定されたビューポート" に対する絶対配置だったので、そのような役割の要素を `position: sticky` で作ろうというのが、基本的な考え方です。

## `position: sticky` でコンテナ要素全体に固定する要素を作る

分かりやすく `position: sticky` な要素を緑色にすると、以下のような感じになります。上下左右にスクロールして、固定したコンテナが動かなければ問題ナシ。

![sticky-container.png](https://qiita-image-store.s3.amazonaws.com/0/70467/0339da3c-17f2-b25b-eb98-22e2bf372c6e.png)

```html
<div class="scrollable-container">
  <!-- コンテナ要素全体に固定する要素 -->
  <div class="sticky-container">
    Sticky container
  </div>
  <div class="scrollable-body">
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
    <p>...</p>
    <p>...</p>
  </div>
</div>
```

```css
.scrollable-container {
  overflow: scroll;
  position: relative;
}

.sticky-container {
  position: -webkit-sticky;
  position: sticky;
  left: 0;
  top: 0;
  z-index: 1;

  width: 100%;
  height: 100%
}

.scrollable-body {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 0;
}
```

ポイントは `<div class="sticky-container">` の位置で、`position: sticky` ナシで普通に配置した時に、**スクロールするコンテナの左上** に来るようにしておきます。

そうしないと、CSS で設定したしきい値を超えずに、`fixed` にならないスクロール位置（いわば「遊び」）ができてしまうことになります。

また、後続の要素もしっかり `height: 100%` の影響を受けるので、<del>ネガティブマージン `margin-bottom: -100%` を使って配置のズレを打ち消しています。</del>

### <ins>2017/02/16: 追記</ins>

前稿では `height: 100%` の影響を打ち消すため、ネガティブマージンを使っていましたが、`margin-bottom: 100%` の `100%` は、**高さではなく、横幅に対する相対指定** でしたので、この指定は誤りでした :sweat_drops:

そのため、後続の要素については、絶対指定・かつ左上固定の `<div class="scrollable-body">` コンテナの中に入れるように修正しています。

## 固定配置したい要素を置く

コンテナが固定できたら、後は `position: absolute` と同じ世界になるので、この中に絶対配置で要素を置いていけば OK です。というわけで、:star: を置きましょう。

```html
<div class="scrollable-container">
  <div class="sticky-container">
    <div class="star">★</div>
  </div>
  <div class="scrollable-body">
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
    <p>...</p>
    <p>...</p>
  </div>
</div>
```

```css
.star {
  position: absolute;
  width: 32px;
  height: 32px;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  margin: auto;

  font-size: 32px;
  line-height: 32px;
  color: #f80;
}
```

![star.png](https://qiita-image-store.s3.amazonaws.com/0/70467/8651f3d7-6dd3-4c8e-9149-ff8d0f597147.png)

無事、スクローラブルなコンテナ内で `fixed` っぽい固定配置ができました。

## 下の要素にアクセスできるようにする

しかし、このままだと『`position: sticky` の要素がコンテナ全体を覆ってしまうため、**下にある要素がクリックできない**』という問題が生じます。

これを回避するためには、固定された要素に [**`pointer-events: none`**](https://developer.mozilla.org/ja/docs/Web/CSS/pointer-events) を指定して、クリックイベントを透過させると良いでしょう。

```css
.sticky-container {
  position: -webkit-sticky;
  position: sticky;
  left: 0;
  top: 0;
  
  width: 100%;
  height: 100%;

  /* クリックイベントを透過 */
  pointer-events: none;
}

.sticky-container > * {
  pointer-events: auto;
}
```

`.sticky-container > *` を使用し、子要素に対して `pointer-events: auto` を適用すれば、固定した要素もちゃんとクリックに反応してくれます。

なので、例えば Android の UI のように、ボタンを右下に固定配置する、なんてこともできたりします。

![material.png](https://qiita-image-store.s3.amazonaws.com/0/70467/7399f7fa-7a9e-fcb3-2ba3-b8dc1e36d7ac.png)

# サンプル

実際のサンプルは :arrow_down: でどうぞ。Firefox / Safari / Chrome 56 以降でご覧ください。

**https://jsfiddle.net/a3k4z409/6/**

Chrome における `position: sticky` サポートが進んだことで、そろそろモダンブラウザでは気兼ねなく使える土壌が整ったのではないでしょうか。Edge の対応が待たれます。