Reactでグローバルメニューを固定してスクロールするとデザインの変わるデザインの実装

目標

Webサイトのデザインにおいて,グローバルメニュー(ヘッダーメニュー)はユーザーがそのサイトを利用する上で一番注目する箇所なのでUXを大きく左右する可能性があります.

今回はスクロール時にグローバルメニューを上に固定し,色を変化(透明→有色)させる実装をReactで行います。

グローバルメニュー gif

方針

まず,グローバルメニューを一つのコンポーネントとして考えるのですが,スクロール時とそうでない時(サイトの一番上にいる時)とで条件分岐して考えたいので,stateを利用します.よって,今回はclassを用いてコンポーネント化します.

実装

今回はTypeScriptを使います.

propsとstateの型宣言

propsは後々使うかもしれませんが今回は空白にしておきます.

type NavbarProps = {

}

type NavbarState = {
  turningScrollpos: any,
  scrolled: boolean
}

stateの初期化

class Navbar extends React.Component<NavbarProps, NavbarState>  {
  constructor(props: NavbarProps) {
    super(props);
    this.state = {
      turningScrollpos: 30,
      scrolled: false
    };
  }

// 以下略
}

ここでturningScrollposがサイトの上からスクロールした量を表しています.よって,サイトの一番上から30だけしたにスクロールした箇所を指します. scrolledはboolean形でtrueの時スクロールした状態を表し,falseはスクロールする前を表すようにしたいので,ここで初期値をfalseとします.(後々,スクロール量が0から30までをfalse, 30以上になる時をtrueになるようにします.)

スクロールした時の状態を関数化

// Adds an event listener when the component is mount.
  // スクロールを認知したらhandleScrollを実行
  componentDidMount() {
    window.addEventListener("scroll", this.handleScroll);
  }

  // Remove the event listener when the component is unmount.
  componentWillUnmount() {
    window.removeEventListener("scroll", this.handleScroll);
  }

  // スクロールしているのか否か判断する関数
  handleScroll = () => {
    // window.pageYOffsetは垂直方向のスクロール量
    // currentScrollPosはスクロールした量
    const currentScrollPos = window.pageYOffset;
    const scrolled = this.state.turningScrollpos < currentScrollPos;

    this.setState({
      scrolled
    });
  };

ここで,componentDidMount()とcomponentWillUnmount()はReactのライフサイクルメソッドです.詳しくはこちらの記事をどうぞ.

また,addEventListenerやremoveEventListenerはJavaScriptからさまざまなイベント処理の実行を可能にするメソッドです.詳しくはこちらの記事をどうぞ.

そしてhandleScrollがスクロールしたのかどうかを判断する関数です.window.pageYOffsetは縦方向のスクロール量を表します.詳しくはこちら. currentScrollPos(スクロールした量)が最初に定めたポイント(turningScrollpos)を上回る時点をターニングポイントとします.

render()部分

render() {
    return (
      <header className={`App-header navbar-fixed ${this.state.scrolled ? "navbar-hidden" : ""}`}>

        <div className="container">
           <div className="name-box" >
             <a href="#">
               <h2 className="site-name">ekubo</h2>
             </a>
           </div>
           <nav className="menu">
             <ul>
                <li><RoundButton link="#" title="サービス概要"/></li>
                <li><RoundButton link="#" title="料金プラン"/></li>
                <li><RoundButton link="#" title="お申し込み"/></li>
                <li><RoundButton link="#" title="講師登録"/></li>
              </ul>
           </nav>
        </div>

      </header>


    );
  }

ここまで長かったですが,目標としてはスクロールした状態なのかどうかを判断したboolean型のscrolledを用意することでした.したがって,render部分ではscrolledを上手く使って表示を切り替えるだけです.今回は三項演算子を使っています.ここで決めたclassNameに従って今回はscssファイルを以下のようにしました.

.navbar-fixed {
  position: fixed;
  width: 100%;
  top: 0;
  transition: 0.6s;
}

.navbar-hidden {
  background-color: $light-cream;
  transition: 0.6s;
}

全コード

最後に,Navbar.tsxの全コードをまとめておきます.

import React, { Props } from 'react';
import './App.scss';
import './Navbar.scss';
import RoundButton from './round-button';

type NavbarProps = {

}

type NavbarState = {
  turningScrollpos: any,
  scrolled: boolean
}

class Navbar extends React.Component<NavbarProps, NavbarState>  {
  constructor(props: NavbarProps) {
    super(props);
    this.state = {
      turningScrollpos: 30,
      scrolled: false
    };
  }

  // Adds an event listener when the component is mount.
  // スクロールを認知したらhandleScrollを実行
  componentDidMount() {
    window.addEventListener("scroll", this.handleScroll);
  }

  // Remove the event listener when the component is unmount.
  componentWillUnmount() {
    window.removeEventListener("scroll", this.handleScroll);
  }

  // スクロールしているのか否か判断する関数
  handleScroll = () => {
    // window.pageYOffsetは垂直方向のスクロール量
    // currentScrollPosはスクロールした量
    const currentScrollPos = window.pageYOffset;
    const scrolled = this.state.turningScrollpos < currentScrollPos;

    this.setState({
      scrolled
    });
  };

  render() {
    return (
      <header className={`App-header navbar-fixed ${this.state.scrolled ? "navbar-hidden" : ""}`}>

        <div className="container">
           <div className="name-box" >
             <a href="#">
               <h2 className="site-name">ekubo</h2>
             </a>
           </div>
           <nav className="menu">
             <ul>
                <li><RoundButton link="#" title="サービス概要"/></li>
                <li><RoundButton link="#" title="料金プラン"/></li>
                <li><RoundButton link="#" title="お申し込み"/></li>
                <li><RoundButton link="#" title="講師登録"/></li>
              </ul>
           </nav>
        </div>

      </header>


    );
  }

}

export default Navbar;

完成形こんな感じ

参考文献

className内で条件分岐 Hide menu when scrolling in React.js window.pageYOffset addEventListenerについて ReactのComponentサイクル

MLエンジニア。教育系のサービスを作っています。