🍆

Intersection Observerでトップに戻るボタンの表示を制御する

2024/03/11に公開

Intersection Observerとは

Intersection Observerは、要素とその親要素やビューポートとの交差状態を非同期で監視するためのWeb API です。要素が画面内に入ってきたり、画面外に出たりした際のイベントを検知できます。

参考記事
https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API
https://ics.media/entry/190902/

やりたいこと

Intersection Observerでボタンのクラスを付与・削除する

  • MainVisualが画面から消えたらトップへ戻るボタンを表示させる
  • Footerが表示されたらトップへ戻るボタンの位置をFooterより上の位置に固定させる

DEMO

https://io-sample.vercel.app/

コード

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>io-sample</title>
  </head>
  <body>
    <header>
      header
    </header>
    <main>
      <section class="mv" id="mv">
        <p>
          MainVisualがviewportから消えるとbtnが表示される
        </p>
      </section>
      <section class="contents">contents</section>
      <section class="contents">contents</section>
      <section class="contents">contents</section>
      <section class="contents">contents</section>
    </main>
    <div class="btn-wrapper" id="btn-wrapper">
      <a href="#" class="btn" id="btn">
        <div class="btn-inner">

        </div>
      </a>
    </div>
    <footer id="footer">
      <p>footerがviewportに入るとbtnの位置がfooter上に固定される</p>
    </footer>
    <script type="module" src="/main.js"></script>
  </body>
</html>

style.css
html {
  scroll-behavior: smooth;  
}

body {
  margin: 0;
  font-family: Helvetica, sans-serif;
  font-size: 16px;
  font-weight: bold;
  text-align: center;
  text-transform: capitalize;
}

header, footer, section {
  display: grid;
  place-items: center;
}

header {
  background-color: rgb(31, 33, 34);
  height: 200px;
  color: white;
}
.mv {
  height: 400px;
  background-color: rgb(180, 193, 204);
}

.contents {
  margin: 20px auto;
  width: 80%;
  height: 400px;
  background-color: rgb(181, 182, 194);
}
footer {
  background-color: rgb(31, 33, 34);
  height: 400px;
  color: white;
}

.btn-wrapper {
  position: relative;
}

.btn {
  position: fixed;
  right: 50px;
  bottom: 100px;
  visibility: hidden;
  pointer-events: none;
  opacity: 0;
  transition: all 0.3s;
}
.btn.active {
  visibility: visible;
  pointer-events: auto;
  opacity: 1;
}
.btn.absolute {
  position: absolute;
  right: 50px;
  bottom: 50px;
}

.btn-inner {
  width: 48px;
  height: 48px;
  background-color: rgb(226, 40, 40);
  opacity: 0.7;
  display: grid;
}
.btn-inner::before {
  justify-self: center;
  align-self: center;
  margin-top: 15px;
  content: '';
  transform: translate(-50%, -50%);
  width: 20px;
  height: 20px;
  border-top: solid 2px;
  border-right: solid 2px;
  transform: rotate(90deg);
  transform: rotate(315deg);
  border-color: white;
}

main.js
import './style.css'

/*==============================================================
  トップへ戻るボタンの表示を切替 intersection observer
==============================================================*/

const btn = document.getElementById('btn');
const mv = document.getElementById('mv');  

const mvOptions = {
  root: null, 
  rootMargin: "0px 0px 0px 0px",
  threshold: 0
};

const mvObserver = new IntersectionObserver(doWhenMvIntersect, mvOptions);
mvObserver.observe(mv);

function doWhenMvIntersect(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting ) {
      btn.classList.remove('active');
    } else {
      btn.classList.add('active');
    }
  });
}

/*==============================================================
 トップへ戻るボタンの位置を変更
==============================================================*/

const footer = document.getElementById('footer');

const footerOptions = {
  root: null, 
  rootMargin: "0px 0px 0px 0px",
  threshold: 0
};

const footerObserver = new IntersectionObserver(doWhenFooterIntersect, footerOptions);

footerObserver.observe(footer);

function doWhenFooterIntersect(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting ) {
      btn.classList.add('absolute');
    } else {
      btn.classList.remove('absolute');
    }
  });
}

解説

監視対象を特定

今回監視対象にidを付与し document.getElementByIdで取得しています。

複数の対象を取得する時はdocument.getElementsByClassNameで取得をします。

document.getElementsByClassNameはHTMLCollectionを返すため、単独で取得したい場合は

const header = document.getElementsByClassName('header')[0];

のように配列の0番目を取得する必要があります。

toggleは使わない

MVを監視対象にした場合、画面更新時に既に交差しているため、一度Intersection Observerが発火してしまいます。

クラス名の付与・削除にはtoggleが便利ですが、画面更新時に一度発火する状態でtoggleを使用すると意図しない挙動になりがちです。

そのため、MVと交差している状態ではremove、交差していない状態ではaddで制御しています。

ボタン位置の固定

footer上にボタンを固定するために、contentsセクションとfooterの間にbtn-wrapperを置き、position: relative;としています。

footerが表示されたらbtnのクラス名に”absolute”が付与されbtn-wrapper基準でbtnの位置が固定されます。

.btn-wrapper {
  position: relative;
}

.btn {
  position: fixed;
  right: 50px;
  bottom: 100px;
}
.btn.absolute {
  position: absolute;
  right: 50px;
  bottom: 50px;
}

補足

scrollイベント

Intersection Observerの登場以前はこうした処理はscrollイベントを用いてスクロール量に応じて任意のクラスを付与する処理が一般的でした。しかし、スクロールする度に計算が走るため処理が重くなる、画面をresizeした時の処理を追加しなければならない、などデメリットがありました。Intersection Observerはscrollイベントのそういったデメリットを解消しています。

Discussion