プログレスバーの作り方(React / Gatsby / Next.js)

blog-hero-imgページの長さによって動的に変わるプログレスバーの簡単な作り方を紹介します。

pen-icon2021.05.11

Profile Pic

この記事の筆者:三好アキ(エンジニア)


ウェブデザイナーから『エンジニア』『プログラマー』へ成長したい人、独学で進んでいきたい人を応援しています。 HTMLとCSSの知識だけでアプリ開発を始められる入門書を多数執筆中📕📗👇


ウェブ制作の教本『はじめてつくるReactアプリ』など複数冊を執筆。



React、Next.js、TypeScriptなどのお役立ち情報や実践的コンテンツを、ビギナー向けにかみ砕いて配信中。登録はこちらから → 無料メルマガ登録

プログレスバーとは?

このページの上部には下のように青色のバーが出ています。

progressbar.jpg

これは「プログレスバー」と呼ばれるもので、ユーザーがスクロールするのに合わせて長さが伸びていき、ページの残りの量を知らせてくれます。

ブラウザの右横のスクロールバーはデスクトップでは常時出ているケースが多いですが、モバイルではスクロールしている時以外は消えています。

そのためプログレスバーは、特にモバイルのユーザーへのUX対策として多くのウェブサイトで導入されているのです。

この記事ではReactのプログレスバーを作っていきます。

記事ページなどのように、記事の文字数や分量によってページの長さが異なるケースに対応するプログレスバーです。

もちろんReactなので、GastbyやNext.jsでも使用できます。

制作するコード

先出しすると、下がこれから作るコードの全体像です。

説明を読んでいる時間のない人は、これをコピーして使ってください。

// progressBar.js

import React, { useState, useEffect, useRef } from 'react'
import { useScrollPosition } from "./useScrollPosition"

const ProgressBar = ({ children }) => {
    const [height, setHeight] = useState(0)
    const ref = useRef(null)

    useEffect(() => {
        setHeight(ref.current.clientHeight)
    }, [])

    const scrollPosition = useScrollPosition()
    
    const percentage = (1 - ((height - scrollPosition) / height)) * 100
    const adjustPercentage = 100 < percentage ? 100 : percentage

    return (
        <div ref={ref}>
            <div className="progress-bar-container">
                <div style={{width: `${adjustPercentage}%`}}></div>
            </div>
            {children}
        </div>
    )
}

export default ProgressBar
// useScrollPosition.js

import { useState, useEffect } from 'react'

export const useScrollPosition = () => {
    const [scrollPosition, setScrollPosition] = useState(0)
    const handleScroll = () => {
        const position = window.pageYOffset
        setScrollPosition(position)
    }
    
    useEffect(() => {
        window.addEventListener('scroll', handleScroll, { passive: true })
        return () => {
            window.removeEventListener('scroll', handleScroll)
        }
    }, [])

    return scrollPosition
}
// progressBar.scss

.progress-bar-container {
    position: fixed;
    top: 100px;
    height: 3px;
    width: 100%; 
    z-index: 10;
    div {
        height: 100%;
        background-color: rgb(0, 255, 255); 
        opacity: 0.7;
    }
}

必要なもの

プログレスバーに必要なものは次の3つです。


• 現在のスクロール位置

• ページのheight

• スクロールの進み具合のパーセント( =「height」から「現在のスクロール位置」を引き、それを「height」で割ったもの)


それではそれぞれ作っていきましょう。

スクロールを位置を求めるカスタムHookを作る

最初に、現在のスクロール位置を特定するファンクションをカスタムHookとして作ります。

以下のコードになります。

// useScrollPosition.js

import { useState, useEffect } from 'react'

export const useScrollPosition = () => {
    const [scrollPosition, setScrollPosition] = useState(0)
    const handleScroll = () => {
        const position = window.pageYOffset
        setScrollPosition(position)
    }
    
    useEffect(() => {
        window.addEventListener('scroll', handleScroll, { passive: true })
        return () => {
            window.removeEventListener('scroll', handleScroll)
        }
    }, [])

    return scrollPosition
}

これで現在のユーザーのスクロール位置がわかるようになりました。

progressBar.jsを作る

次にプログレスバーのファイル、progressBar.jsを作りましょう。

そこにベースとなる次のコードを打ちます。

// progressBar.js

import React, { useState, useEffect, useRef } from 'react'

const ProgressBar = () => {
    return (
        <div>

        </div>
    )
}

export default ProgressBar

ページのheightを求める

次にrefuseStateuseEffectを使ってページの高さを求めます。

ここではブログの記事ページなどでの使用を想定しており、記事の文章量によってページの長さも変わってくるのでrefが必要です。

// progressBar.js

import React, { useState, useEffect, useRef } from 'react'

const ProgressBar = ({ children }) => {
    const [height, setHeight] = useState(0)
    const ref = useRef(null)

    useEffect(() => {
        setHeight(ref.current.clientHeight)
    }, [])

    return (
        <div ref={ref}>

        </div>
    )
}

export default ProgressBar

スクロールの進み具合を求める

先ほど作ったuseScrollPositionを使って、スクロールの進み具合を出します。

// progressBar.js

import React, { useState, useEffect, useRef } from 'react'
import { useScrollPosition } from "./useScrollPosition"

const ProgressBar = ({ children }) => {
    const [height, setHeight] = useState(0)
    const ref = useRef(null)

    useEffect(() => {
        setHeight(ref.current.clientHeight)
    }, [])

    const scrollPosition = useScrollPosition()
    
    const percentage = (1 - ((height - scrollPosition) / height)) * 100
    const adjustPercentage = 100 < percentage ? 100 : percentage

    return (
        <div ref={ref}>

        </div>
    )
}

export default ProgressBar

上のコードのadjustPercentageがスクロールの進み具合になり、100 < percentage ? 100 : percentageでパーセントが100を超えないよう制御しています。

adjustPercentageの数値をページの幅(width)に設定すると、スクロールする毎にバーが伸びていきます。

// progressBar.js

...

return (
    <div ref={ref}>
        <div className="progress-bar-container">
            <div style={{width: `${adjustPercentage}%`}}></div>
        </div>
    </div>
)

...

そしてprogressBarコンポーネントは、使いたいページのコンポーネントを包むように配置する必要があるのでchildrenが必要になります。

progressBar.js全体は以下のようになっています。

// progressBar.js

import React, { useState, useEffect, useRef } from 'react'
import { useScrollPosition } from "../hooks/useScrollPosition"

const ProgressBar = ({ children }) => {
    const [height, setHeight] = useState(0)
    const ref = useRef(null)

    useEffect(() => {
        setHeight(ref.current.clientHeight)
    }, [])

    const scrollPosition = useScrollPosition()
    
    const percentage = (1 - ((height - scrollPosition) / height)) * 100
    const adjustPercentage = 100 < percentage ? 100 : percentage

    return (
        <div ref={ref}>
            <div className="progress-bar-container">
                <div style={{width: `${adjustPercentage}%`}}></div>
            </div>
            {children}
        </div>
    )
}

export default ProgressBar

CSSを書く

最後にCSSを書きましょう。

ここではSCSSを使っています。

// progressBar.scss

.progress-bar-container {
    position: fixed;
    top: 100px;
    height: 3px;
    width: 100%; 
    z-index: 10;
    div {
        height: 100%;
        background-color: rgb(0, 255, 255); 
        opacity: 0.7;
    }
}

progressBar.jsの使い方

これを下のように使いたいページで読み込みます。

ここではブログ記事のコンポーネントに読み込むことを想定しています。

// singlePost.js

import React from 'react'
import Layout from "./layout"
import ProgressBar from "./progressBar"
import PostBody from "./postBlody"
import LatestPosts from "./latestPosts"

const SinglePost =() => {
    return (
        <Layout>
            <ProgressBar>
                <PostBlody/>
                <latestPosts/>
            </ProgressBar>
        </Layout>
    )
}

export default SinglePost