Closed2

スクロール位置に応じて要素のサイズをなめらかに変える

catnosecatnose

フォントサイズや要素の幅・高さをスクロール量に応じてなめらかに変えたいというケースがごく稀にある。

上の例では最初は60pxだが、現在の位置より上にスクロールされると少しずつサイズが小さくなり、要素がビューポート上部に達したときに20pxになるように実装されている。

これをReactで実装したのだが、結局使わなかったので供養しておく。

catnosecatnose

1. 要素の位置を取得するカスタムフックを作る

複数の要素に対して同じことをやりたくなったときのために、要素の位置を取得する処理はカスタムフックに切り出しておく。

useOffsetTop.tsx
import React, { useEffect, useState, useCallback } from "react";

export function useOffsetTop(ref?: React.RefObject<HTMLElement>) {
  const [viewportOffsetTop, setViewportOffsetTop] = useState<number | undefined>(undefined);
  const [pageOffsetTop, setPageOffsetTop] = useState<number | undefined>(
    undefined
  );

  const handler = useCallback(() => {
    if (!ref?.current) return;
    const clientRect = ref.current.getBoundingClientRect();
    setViewportTop(clientRect.top);
    const newPageOffsetTop = clientRect.top + window.pageYOffset;
    if (newPageOffsetTop !== pageOffsetTop) setPageOffsetTop(newPageOffsetTop);
  }, [ref]);

  useEffect(() => {
    window.addEventListener("scroll", handler);

    return () => window.removeEventListener("scroll", handler);
  }, [ref, handler]);

  return { viewportTop, pageOffsetTop };
}
  • 引数には位置を調べたい要素のRefObjectを渡す。
  • 返り値のviewportTopは現在表示されているビューポートの上端を起点にしたオフセット位置を表す。
  • スクロールで通りすぎたときには(ビューポート上端より上にある場合)はマイナスの値となる。
  • pageOffsetTopはスクロール量に関係なく、ページ上端からの位置を表す。
  • ここでは省略しているが、繰り返しレンダリングが発生することによる負荷を減らすために、handler()にはThrottleを設定して呼び出される頻度を減らすと良い

2. 位置に応じてCSSのプロパティを変える

ExampleHeader.tsx
import { useRef, useMemo } from "react";

const maxIconSize = 60; // 最大のアイコンサイズ
const minIconSize = 20; // 最小のアイコンサイズ

export const Icon: React.VFC = () => {
  const iconRef = useRef(null);
  const { pageOffsetTop, viewportTop } = useOffsetTop(iconRef);

  // 要素の位置をもとにサイズを計算
  const iconSize = useMemo(() => {
    if (pageOffsetTop === undefined || viewportTop === undefined)
      return maxIconSize;
    const size = (viewportTop / pageOffsetTop) * maxIconSize;
    if (size <= minIconSize) return minIconSize;
    return size.toFixed(1);
  }, [pageOffsetTop, viewportTop]);


  return (
    <div
      className="icon"
      ref={iconRef}
      style={{
        width: `${iconSize}px`,
	height: `${iconSize}px`,
      }}
    />
  );
};

これで最初は要素の幅・高さは60px(maxIconSizeの値)だが、ビューポート上部に達したときには20px(minIconSizeの値)となる。その中間のスクロール位置ではスクロール量に応じた幅・高さとなる。

👆 ちなみにこの例では、CSSでposition: stickytop: 0を指定することでビューポート上部に達した後はページ上部に追尾されるようにしている。

このスクラップは2021/11/14にクローズされました