みかづきブログ・カスタム

基本的にはちょちょいのほいです。

position: stickyとscrollIntoViewを組み合わせて使う 💻

position: stickyを使うと、簡単に吸着されるようなページスクロールを実装できます。

developer.mozilla.org

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を使ってかけば、もっとシンプルになります。