👒

TS/CSSでスクロール時にヘッダーをしまう

2024/11/29に公開

はじめに

ページを上にスクロールすると上に完全にしまわれ、下にスクロールすると上から完全に出てくるヘッダーを作りました。

実装

ヘッダーコンポーネントに対し、スクロールが発生したら hide のスタイルを付けたり外したりしています。

layout.tsx
...

const Layout = ({ children }: { children: React.ReactNode }) => {
  const mainRef = useRef<HTMLElement>(null);
  // スクロール方向を判定するために直前のスクロール位置を保存
  // ヘッダーが縦68pxなので、初期位置は上から68px
  const [prevPosition, setPrevPosition] = useState<number>(68);
  const [isHiddenHeader, setIsHiddenHeader] = useState<boolean>(false);

  const onScroll = useCallback(() => {
    if (!mainRef?.current) return;
    const top = mainRef?.current.getBoundingClientRect().top;
    // 直前の位置より上にいる、かつ-30pxより上の位置にいたらヘッダーをしまう
    // 初期位置からすぐにしまうと、メインコンテンツの上部にたくさん空白ができてしまうので、0px〜-30pxの位置ではしまわないように
    if (top < prevPosition && top < -30) {
      setIsHiddenHeader(true);
    } else {
      setIsHiddenHeader(false);
    }
    setPrevPosition(top);
  }, [prevPosition]);

  // スクロールイベントを取得
  useEffect(() => {
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, [onScroll]);

  return (
    <>
      <Header isHidden={isHiddenHeader} />
      <main className={styles.main} ref={mainRef}>
        {children}
      </main>
      <Footer />
    </>
  );
};
Header.tsx
...

type Props = {
  isHidden?: boolean;
};

export const Header = ({ isHidden = false }: Props) => {
  return (
    // isHiddenが付与された時だけ隠す用のスタイルを追加
    <div className={`${styles.component} ${isHidden && styles.hide}`}>
      <Link href="/">
        <Image
          className={styles.logo}
          src="/logowithtitle.png"
          alt="logo image"
          width={253}
          height={32}
          priority
        />
      </Link>
    </div>
  );
};

Header.module.css
.component {
  width: 100%;
  height: 68px;
  position: fixed;
  top: 0px;
  // topが変化したらアニメーションさせる
  transition: top 0.3s;
  z-index: 999;
}

.hide {
  // ヘッダーの高さ分上に移動させる
  top: -68px;
}

おわりに

記事の内容とは関係ありませんが、bool値の変数名にいつも迷います。
今回も「headerを隠すかどうか」を変数にしていますが、「ヘッダーを隠す」と英語にすると hideHeader になって関数名っぽくなってしまい、「ヘッダーが隠れている」だと headerIsHiddenis が中間に来ていることが違和感・・・。
結局 isHeaderHidden にして、英語の語順的には変な気がするけど is を先頭に来させるという統一感でやりました。

逆にきれいな変数名にならないということは、変数名を作る基準が違っているのかもしれない。
今回は「ヘッダーを隠すかどうか」ではなく「上スクロールかどうか」で isScrolledUp、もしくはbool値ではなく scrollDirection: 'UP' | 'DOWN' | 'NONE' みたいに定数を返すものにしたらよかったか?

Discussion