LoginSignup
7
2
はじめての記事投稿

【React(TypeScript) + Chakra UI】フッターと重ならない「トップに戻るボタン」を作成する

Last updated at Posted at 2023-07-10

作成するもの

  • トップに戻るボタン
    • クリックするとページのトップに戻る
    • ページトップでは表示されない
    • 画面右下に固定表示される。
    • 画面内に少しでもフッターが表示されると固定表示が解除され
      ボタンはフッターの上に表示される。

上に戻るボタン.gif

前提

  • TypeScript のバージョンが 4.1.0 以上であること。(理由については こちら を参照)
  • React がインストールされていること。
  • Chakra UI がインストールされていること。
  • Chakra UI icons がインストールされていること。
    (ボタンのアイコンに自分で用意した画像を使用する場合はインストール不要)

成果物

footer.tsx
import { Box } from "@chakra-ui/layout";
import { Fade, IconButton, ResponsiveValue } from "@chakra-ui/react";
import { ArrowLeftIcon } from "@chakra-ui/icons";
import React, { useState, useLayoutEffect, useRef } from "react";

interface ScrollToTopButtonProps {
  footerHeight     : number
  position         : ResponsiveValue<any>
}

const ScrollToTopButton = (props: ScrollToTopButtonProps) => {
  const handleClick = () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
  };

  return (
    <IconButton
      aria-label  ="Scroll to top"
      icon        ={<ArrowLeftIcon transform="rotate(90deg)" w="4" h="4"/>}
      position    ={props.position}
      bottom      ={props.position === "fixed" ? "8" : props.footerHeight + 20}
      right       ="8"
      size        ="lg"
      borderRadius="full"
      color       ="white"
      opacity     ="30%"
      bg          ="gray.600"
      _hover      ={{ opacity: "70%", bg: "gray.500" }}
      onClick     ={handleClick}
    />
  );
};

const Footer = () => {
  const footerRef = useRef<HTMLDivElement>(null);
  const [pageHeight       , setPageHeight]        = useState<number>(0);
  const [windowInnerHeight, setWindowInnerHeight] = useState<number>(0);
  const [scrollY          , setScrollY]           = useState<number>(0);
  const [footerHeight     , setFooterHeight]      = useState<number>(0);
  const [position         , setPosition]          = useState<ResponsiveValue<any> | undefined>("fixed");

  const determineButtonPosition = () => {
    setPageHeight(document.body.offsetHeight);
    setWindowInnerHeight(window.innerHeight);
    setScrollY(window.scrollY);
    setFooterHeight(footerRef.current?.clientHeight ?? 0);

    if (pageHeight - windowInnerHeight - scrollY <= footerHeight) {
      setPosition("absolute");
    } else {
      setPosition("fixed");
    }
  };

  useLayoutEffect(() => {
    window.addEventListener("resize", determineButtonPosition);
    window.addEventListener("scroll", determineButtonPosition);
    return () => {
      window.removeEventListener("resize", determineButtonPosition);
      window.removeEventListener("scroll", determineButtonPosition);
    };
  });

  return (
    <Box as="footer" h= "30dvh" bg="red.50" fontSize="36px" ref={footerRef}>
      height = 30dvh のフッター
        <Fade in={scrollY > 0}>
          <ScrollToTopButton footerHeight     ={footerHeight}
                             position         ={position}
          />
        </Fade>
    </Box>
  );
};

export default Footer;

以下、コードの解説

0. ボタンの位置を制御するための方針

  • ボタンの表示パターンは3つ
ボタンの表示パターン 条件
表示なし 縦方向のスクロール量 = 0
画面右下に表示 縦方向のスクロール量 > 0
かつ
画面内にフッターが全く表示されていない
フッターの上に表示 縦方向のスクロール量 > 0
かつ
画面内にフッターが少しでも表示されている
  • 上記パターンの判定に必要なパラメータ
変数名 説明
pageHeight ページ全体の高さ
windowInnerHeight ブラウザに表示されているページの高さ
scrollY 縦方向のスクロール量
footerHeight フッターの高さ
  • 画面内にフッターが表示されているかどうかの判定
    • ➀ - ➁ - ➂
      ⇒画面内にフッターが 表示されている
    • ➀ - ➁ - ➂
      ⇒画面内にフッターが 表示されていない

※ページ中間までスクロールした例
(赤矢印は ➀ - ➁ - ➂ であり、は点線部の高さを含まない。)
Qiita説明用赤矢印付き.png

1. ボタンのコンポーネント(ScrollToTopButton)

1.1 ScrollToTopButtonのprops

前述の footerHeight に加え、ボタンの表示位置を制御する position を持つ。

Footer.tsx
interface ScrollToTopButtonProps {
  footerHeight     : number
  position         : ResponsiveValue<any>
}

1.2 ScrollToTopButtonの実装

ボタンクリック時に handleClick 関数が呼び出され、ページトップまで滑らかに遷移する。
ボタンの表示位置は propsposition の設定値に応じて、以下の通り変化させる。

  • position"fixed"
    ⇒ボタンは画面右下に固定表示

  • position"fixed" 以外
    ⇒ボタンはフッターよりも少し上に表示

Footer.tsx
const ScrollToTopButton = (props: ScrollToTopButtonProps) => {
  const handleClick = () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
  };

  return (
    <IconButton
      aria-label  ="Scroll to top"
      icon        ={<ArrowLeftIcon transform="rotate(90deg)" w="4" h="4"/>}
      position    ={props.position}
      bottom      ={props.position === "fixed" ? "8" : props.footerHeight + 20}
      right       ="8"
      size        ="lg"
      borderRadius="full"
      color       ="white"
      opacity     ="30%"
      bg          ="gray.600"
      _hover      ={{ opacity: "70%", bg: "gray.500" }}
      onClick     ={handleClick}
    />
  );
};

2.フッターのコンポーネント(Footer)

2.1 フックの定義

Reactのフックを使用して、フッターの情報を格納する変数と
ボタンの表示パターンの判定に必要な変数を定義する。

Reactのフックについては公式のリファレンスを参照。

Footer.tsx
  const footerRef = useRef<HTMLDivElement>(null);
  const [pageHeight       , setPageHeight]        = useState<number>(0);
  const [windowInnerHeight, setWindowInnerHeight] = useState<number>(0);
  const [scrollY          , setScrollY]           = useState<number>(0);
  const [footerHeight     , setFooterHeight]      = useState<number>(0);
  const [position         , setPosition]          = useState<ResponsiveValue<any> | undefined>("fixed");

2.2 ボタンの表示位置を決定する

ウィンドウのサイズ変更 または スクロールが発生する度に determineButtonPosition を呼び出す。
ボタンの表示パターンの判定に必要な各パラメータを取得し、ボタンの表示位置を決定する。

Footer.tsx
  const determineButtonPosition = () => {
    setPageHeight(document.body.offsetHeight);
    setWindowInnerHeight(window.innerHeight);
    setScrollY(window.scrollY);
    setFooterHeight(footerRef.current?.clientHeight ?? 0);

    if (pageHeight - windowInnerHeight - scrollY <= footerHeight) {
      setPosition("absolute");
    } else {
      setPosition("fixed");
    }
  };

  useLayoutEffect(() => {
    window.addEventListener("resize", determineButtonPosition);
    window.addEventListener("scroll", determineButtonPosition);
    return () => {
      window.removeEventListener("resize", determineButtonPosition);
      window.removeEventListener("scroll", determineButtonPosition);
    };
  });

2.3 Footerコンポーネントの定義

determineButtonPosition 関数がフッターの情報を取得できるよう、propsにref を設定する。
また、ページトップでボタンが表示されないよう、ScrollToTopButton コンポーネントを
<Fade> の子要素として配置する。

Footer.tsx
  return (
    <Box as="footer" h= "30dvh" bg="red.50" fontSize="36px" ref={footerRef}>
      height = 30dvh のフッター
        <Fade in={scrollY > 0}>
          <ScrollToTopButton footerHeight     ={footerHeight}
                                 position     ={position}
          />
        </Fade>
    </Box>
  );

Chakra UIの <Fade> については公式のリファレンスを参照。

参考サイト

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2