position: stickyを使うと、簡単に吸着されるようなページスクロールを実装できます。
DEMO
非常に手軽です。
しかし、ここにページ内リンクをscrollIntoViewを使って実装するとちょっとした問題が起こります。
具体的にいうと、現在位置よりも下にある要素には問題なくスクロールされて頭出しされるのですが、上方向には動きません。
なぜならば、DEMOでは、position: stickyが設定されている要素のtopに0が設定されているからです。
つまり、すでに頭出しされている状態なので動かないわけです。
対策を入れたDEMO
こちらが上方向への頭出しにも対応したDEMOです。
具体的に何をしているかというと、
- ❶ stickyを設定している要素と同じ高さの要素をつくる
- ❷ ❶でつくった要素のpositionをabsoluteにして、topをstickyを設定している要素と合わせる
- ❸ Windowがリサイズされるたびに❶でつくった要素も同じ高さにリサイズする
- ❹ ページ内リンクがクリックされた際は、❶でつくったDOMのscrollIntoViewを実行する
- ❺ ❶でつくった要素のpointer-eventsをnoneにする
ということをしています。
HTML
<header> <ol> <li data-index="1">1</li> <li data-index="2">2</li> <li data-index="3">3</li> <li data-index="4">4</li> <li data-index="5">5</li> </ol> </header> <main> <ol class="sections"> <li> <section>1</section> </li> <li> <section>2</section> </li> <li> <section>3</section> </li> <li> <section>4</section> </li> <li> <section>5</section> </li> </ol> <ol id="spacer"></ol><!--❶--> </main>
JavaScript
let timer = -1; handleResize(); document.querySelector('header > ol').addEventListener('click', (evt) => { const index = evt.target.dataset.index; if (index) { const target = document.querySelector(`#spacer > li:nth-child(${ index })`); // ❹ if (target) { target.scrollIntoView({ behavior: 'smooth' }); } } }); window.addEventListener('resize', handleResize); function handleResize() { const delay = 400; window.clearTimeout(timer); timer = setTimeout(() => { const targets = document.querySelectorAll('.sections li'); let html = ''; [].slice.call(targets).forEach((target) => { html += `<li style="height:${ target.clientHeight }px"></li>`; }); document.getElementById('spacer').innerHTML = html; // ❸ }, delay); }
CSS
$colors: #C2185B, #D32F2F, #F57C00, #0288D1, #7B1FA2; body { color: #FAFAFA; } header { ol { display: flex; align-items: center; flex-direction: column; justify-content: center; position: fixed; top: 0; bottom: 0; left: 0; width: 40px; z-index: 1; } li { margin: 8px; text-decoration: underline; cursor: pointer; } } main { .sections { li { position: sticky; top: 0; width: 100vw; height: 100dvh; section { display: flex; align-items: center; justify-content: center; width: 100vw; height: 100%; font-size: 40px; } @each $color in $colors { $index: index($colors, $color); &:nth-child(#{$index}) { background: $color; } } } } } #spacer { position: absolute; // ❷ top: 0; z-index: -1; pointer-events: none; // ❺ }
やや複雑なコードになってしまっていますが、ReactやViewを使ってかけば、もっとシンプルになります。