Edited at

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

More than 1 year has passed since last update.


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

先日リリースされた Chrome 56 で position: sticky が戻ってきた のを記念して、position: sticky を活用した小技です。

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


やりたいこと

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

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

scrollable-container.png


HTML

<div class="scrollable-container">

<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
<p>...</p>
<p>...</p>
</div>


CSS (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


position: absolute

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

position-absolute.png

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


position: sticky とは

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

詳しい説明が MDN にありますが、正直、言葉じゃちんぷんかんなので、デモページなどを見ると感じが掴めるのではないかと思います。

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


対応状況

Can I use で見てみてください。 Firefox はとうの昔から対応していましたが、Chrome は experimental な実装のまま、一度ドロップした経緯があります。52 で再度 experimental で実装され、先日の 56 のリリースで晴れて正式な実装となりました。(参考: Chrome 56 に position: sticky; が戻ってきた)

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

<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>

.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% の影響を受けるので、ネガティブマージン margin-bottom: -100% を使って配置のズレを打ち消しています。


2017/02/16: 追記

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

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


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

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

<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>

.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

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


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

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

これを回避するためには、固定された要素に pointer-events: none を指定して、クリックイベントを透過させると良いでしょう。

.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


サンプル

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

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

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