iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

ヘッダーを固定した縦横スクロールを実装する

はじめに

こんにちは、株式会社iimonでフロントエンドエンジニアをしている木村です。

本記事はアドベントカレンダー14日目の記事になります!

cssって奥が深いですよね。勉強した気になっても自分の思い通りに一発でなってくれたことがありません。そんな中で、以前いつにも増して詰まった実装がありました。

「スクロールバーを縦と横に常時表示する」というものです。

入社して1ヶ月経たないぐらいに取り組んだこの実装、あまり理解できずに実装していた部分も大きいです。入社して半年と少し経った今なら、もっとちがう方法で、ちゃんと理解して実装できるんじゃないかとこの記事を書いてみました。

まだまだ勉強中のため、間違い等ありましたらご指摘お願い致します!

やりたいこと

  • 固定の要素の中に大きな要素を入れて、ヘッダーは固定しつつ縦横にスクロールを常時つけたい
  • 横のスクロールをした時はヘッダーも一緒にスクロールする
  • ヘッダーにはスクロールバーをつけない
  • 縦にスクロールしてもヘッダーが常に見える


方法

結論からまとめると、(自分が実装できる)方法は2つありました。

1. スクリプトを使用した実装
2. ReactでuseRefを使用した実装

以下、説明の簡略化のためにヘッダーの下に表示する縦横に大きい要素のことをcontent要素と呼びます。

方法1:スクリプトを使用しての実装

CSSの入門を一通り済ませたらスクロールの付け方はなんとなくわかると思いますが、overflow-x:scroloverflow-y:scroll をつける、またはoverflow:scrolで隠れる部分がある場合にスクロールで要素を表示することができます。

<div class="main">
    <div class="headerWrapper">
      <div class="header">ヘッダー</div>
    </div>
    <div class="longContentWrapper">
      <div class="longAndWideContent">メイン部分</div>
    </div>
</div>
body {
  height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
}

.main {
  display: grid;
  height: 300px;
  width: 500px;
  border: 1px solid black;
}


.header {
  background-color: pink;
  width: 1000px;
  height: 80px;
}

.longAndWideContent {
  width: 1000px;
  height: 1000px;
  background-color: skyblue;
}

.longContentWrapper {
  overflow: scroll;
}

.headerWrapper {
  overflow-x: scroll;
  overflow-y:hidden;
}


codepen.io

ただ、この状態だとヘッダーとメインが横スクロールした時に一緒になりません。

ヘッダー部分までスクロールバーがついてもいいのなら、position:sticyを使用して固定して実装することが出来ます。

<div class="main">
  <div class="header">ヘッダー</div>
  <div class="longAndWideContent">大きなコンテンツ</div>
</div>
body {
  height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
}

.main {
  position: relative;
  height: 300px;
  width: 500px;
  border: 1px solid black;
  overflow: auto; 
}

.header {
  position: sticky;
  top: 0;
  background-color: pink;
  width: 1000px;
  height: 80px;
  z-index: 1;
}

.longAndWideContent{
  width: 1000px;
  height: 1000px;
  background-color: skyblue;
}

codepen.io


どうしてもヘッダーにスクロールバーを付けたくない!下のコンテンツの範囲だけに縦のスクロールバーが欲しい!というとき(私の場合)はスクリプトでヘッダーのスクロールを制御することで実装することが出来ます。

<div class="main">
  <div class="headerWrapper">
    <div class="header">ヘッダー</div>
  </div>

  <div class="longContentWrapper" onscroll="syncScroll()">
    <div class="longAndWideContent">大きなコンテンツ</div>
  </div>
</div>
body {
  height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
}

.main {
  display: grid;
  height: 300px;
  width: 500px;
  border: 1px solid black;
}

.headerWrapper {
  overflow-x: scroll;
  overflow-y: hidden;
}

.header {
  background-color: pink;
  width: 1000px;
  height: 80px;
}

 .headerWrapper::-webkit-scrollbar {
    display: none;
    height: 0px;
  }

.longContentWrapper {
  overflow: auto;
  height: 100%;
  width: 100%;
}

.longAndWideContent {
  width: 1000px;
  height: 1000px;
  background-color: skyblue;
}

メイン部分のスクロール位置をスクロール位置をヘッダーに反映するスクリプトを、メイン部分のonscrollに仕込みます。これでメイン部分を横にスクロールしたときに反映されるようになりました。

ただ、このままだとヘッダー部分にスクロールバーがついてちょっと不格好なのでヘッダーのスクロールをなくします。

Chromesafariでは::-webkit-scrollbar{display:none} でスクロールバーを非表示にできるので、このスタイルをヘッダーを入れている要素(この場合はheaderWrapper)につければ完成です。

codepen.io

方法2:React/useRefを使用しての方法

自分が当時実装したのがこちらの方法です。ただ、useRefがどんなものなのかもきちんと理解せずなんとなくで進めてしまった反省があるので、この機会に改めてuseRefについて調べて実装を行いました。

Reactやその他Hooksについての説明は割愛させていただきます。

React/useRef

useRefは.currentプロパティが渡された引数をrefObjectに返すフックスです。

かみ砕くと、useRefを参照させたい箇所に設置すると、その場所の情報を動的に保持してくれます。

以下のコードでは、方法1のsyncScroll関数で行っていることをuseRefを使用した形で置き換えています。

1. useRefを初期値nullで呼び出します。
2. useRefをヘッダーの入れ物(headerWrapper)と メイン部分の入れ物(longContentWrapper)のref属性に渡すことで、それぞれの要素の位置を動的に取得できるようにします。
3. 方法1と同様に、メイン部分の入れ物にonscrollでsyncScrollを設定します。
4. syncScrollでヘッダー部分の位置にメイン部分の位置を反映させます。

(今回の実装に直接的には関係ありませんが)ReactでuseStateなどを使うと、値が変更されたタイミングで再レンダリング(DOMの再描画)が行われますが、useRefは値が変更されても再レンダリングされません。なのでuseRefの値を表示させてみると、初期値nullのままで、スクロールしても変化することがありません

codepen.io

import React, { useRef } from "https://esm.sh/react@18";
import ReactDOM from "https://esm.sh/react-dom@18";

function App() {
  const headerRef = useRef(null);
  const contentRef = useRef(null);

  const syncScroll = () => {
      headerRef.current.scrollLeft = contentRef.current.scrollLeft;
  };

  return (
    <body>
        <div className="main">
      <div className="headerWrapper" ref={headerRef}>
        <div className="header">ヘッダー</div>
      </div>

      <div className="longContentWrapper" ref={contentRef} onScroll={syncScroll}>
        <div className="longAndWideContent">大きい要素</div>
      </div>
    </div>
      <div>{`headerRef:${headerRef.current}`}</div>
      <div>{`contentRef:${contentRef.current}`}</div>
  </body>
  );
}
//htmlにIDがrootの要素を用意している前提
ReactDOM.render(<App />,
document.getElementById("root"))

さいごに

要素がどういう風に組み立てられているかというのがあまり感覚としてなかったときに実装したのでどうにかCSSだけで実装できると思い込んでいました。違う要素を連動させる、という動きを作りたいときは素直にスクリプトに頼ったほうが早いなと思いました。

私が勉強不足なだけでCSSだけでも実装できる!という方がいらっしゃいましたら是非教えていただけたらと思います!


現在弊社ではエンジニアを募集しています!

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

最後まで読んでいただきありがとうございました!

次のアドベントカレンダーの記事はさいとーさんです! 記事が出るのが楽しみですね!