📜

スクロールバーを使わなくてもScrollableは表現できる!useScrollableフックとCSSを使った実装例

2021/10/03に公開

サービスの管理画面やチャット画面の実装をしていた時に、Scrollable(スクロール可能)なUIを実装する機会があったので記事にまとめました。

スクロールバーだけではないScrollableなUIを考えるきっかけにしていただけたらと思います。

↕️ スクロールバー

もちろん、スクロールバーはユーザーがパッとみてScrollableだと感じるUIの代表です。

ただ、スクロールバーをカスタマイズしないとブラウザによって見え方が異なることがあるため、Figmaなどでデザインしたものと異なる見た目に感じてしまう。といったことが考えられます。

この記事ではスクロールバーには触れず、
別の方法でScrollableなUIをReact Hooksで実装する方法について説明します。

🎨 グラデーション

CSSの linear-gradient を使用する方法です。

今回作成したUIをCodeSandboxで作成してみました。

UIの解説

背景にグラデーションを持たせることで、コンテンツが上や下に存在していること(=Scrollable)を表現しています。

下にスクロール可能であれば、コンテンツの下にグラデーションがかかり、
上にスクロール可能であれば、コンテンツの上にグラデーションがかかる、
というUIです。

よく有料記事の続きを読む、で使用されていて目にすることは多いのでユーザーが自然に感じるUIかと思います。


[パーツ]「続きを読む」以下をグラデーションで隠すボタン[1]

このUIを使用するタイミング

Zennやニュース記事など文章量が多く高さが固定ではない場合、スクロールバーの有無に関わらずScrollableだとユーザーは感じることが多いため、あまり効果がなさそうです。

反対に、ページ内のある領域に高さ固定でスクロール可能なコンポーネントが存在している場合ユーザーに、スクロールバーよりもScrollableだと気づいてもらえる確率が上がると思います。

React Hooks での実装

実装についての説明です。
日頃は React, TypeScript を書いているので Custom Hook を作ってみました。

実装は比較的シンプルで、スクロール要素の scrollTop, clientHeight, scrollHeight を用いて上下方向にスクロール可能かを計算します。

useScrollable というCustom Hookを作成しました。
RefObject<HTMLElement> を引数にとり、上下方向それぞれにスクロール可能かを Boolean で返します。

下記の例ではスクロールイベントが発火する度に計算を行うようにしていますが、
throttle を使用して処理を間引いてあげても良さそうです。

useScrollable.ts
import { RefObject, useState, useEffect } from "react";

export type State = {
  top: boolean;
  bottom: boolean;
};

export const useScrollable = (ref: RefObject<HTMLElement>): State => {
  const [state, setState] = useState<State>({ top: false, bottom: false });

  useEffect(() => {
    const handleScroll = () => {
      if (ref.current) {
        const { scrollTop, scrollHeight, clientHeight } = ref.current;
        setState({
          top: scrollTop > 0,
          bottom:
            // そもそもスクロールが不可能な場合は除き
            clientHeight !== scrollHeight &&
            // 少しでもスクロールしている場合
            scrollTop + clientHeight < scrollHeight
        });
      }
    };
    handleScroll();

    if (ref.current) {
      ref.current.addEventListener("scroll", handleScroll);
    }

    return () => {
      if (ref.current) {
        ref.current.removeEventListener("scroll", handleScroll);
      }
    };
  }, [ref.current]);

  return state;
};

ComponentやCSSも置いておきます。

Component
export const App = () => {
  const scrollableElem = useRef<HTMLDivElement>(null);
  const { top, bottom } = useScrollable(scrollableElem);

  return (
    <Wrap top={top} bottom={bottom}>
      <ScrollContent ref={scrollableElem}>
        日本国民は正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民と協和による成果と、わが国全土にわたって自由のもたらす恵沢を確保し、政府の行為によって再び戦争の惨禍が起こることのないようにすることを決意し、ここに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は国民の厳粛な信託によるものであって、その権威は国民に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、かかる原理に基づくものである。われらはこれに反する一切の憲法、法令及び詔勅を排除する。
        日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであって、平和を愛する諸国民の公正と信義を信頼して、われらの安全と生存を保持しようと決意した。われらは平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去しようと努めている国際社会において、名誉ある地位を占めたいと思う。われらは全世界の国民が、ひとしく恐怖と欠乏から免れ、平和の内に生存する権利を有することを確認する。
        われらは、いずれの国家も、自国のことのみに専念して他国を無視してはならないのであって、政治道徳の法則は、普遍的なものであり、この法則に従うことは、自国の主権を維持し、他国と対等関係に立とうとする各国の責務であると信ずる。
        日本国民は、国家の名誉にかけて、全力をあげて崇高な理想と目的を達成することを誓う。
      </ScrollContent>
    </Wrap>
  );
}
CSS
styles.ts
import styled from "styled-components";

export type DivStyleProps = {
  top: boolean;
  bottom: boolean;
};

export const Wrap = styled.div`
  height: 200px;
  position: relative;

  &::before {
    content: "";
    display: block;
    position: absolute;
    height: 20px;
    top: 0;
    left: 0;
    right: 0;
    background: linear-gradient(
      180deg,
      rgba(210, 210, 210, 0.8) 0,
      rgba(210, 210, 210, 0) 50%
    );
    opacity: ${({ top }: DivStyleProps) => (top ? 1 : 0)};
    transition: opacity 0.25s ease-in-out;
  }
  &::after {
    content: "";
    display: block;
    position: absolute;
    height: 20px;
    bottom: 0;
    left: 0;
    right: 0;
    background: linear-gradient(
      0deg,
      rgba(210, 210, 210, 0.8) 0,
      rgba(210, 210, 210, 0) 50%
    );
    opacity: ${({ bottom }: DivStyleProps) => (bottom ? 1 : 0)};
    transition: opacity 0.25s ease-in-out;
  }
`;

export const ScrollContent = styled.div`
  width: 100%;
  height: 100%;
  overflow: scroll;
`;

💬 まとめ

スクロールバーを使用しないでScrollableなUIについて考えてみました。

ここで紹介したUIをCSSのみで実現する方法もあるので参考程度に載せておきます。
CSSのみで実現は可能と思ったのですが、スマホやIEでうまく表示されないことがあるので使用には注意が必要かもしれません。Chromeだと問題なさそうです。

僕個人のことになりますが、最近はユーザーにストレスを感じさせないUIを意識しながらコンポーネントを作っています。
ここでも使用した linear-gradient を使用してアコーディオンのUIを改善してみたので、次回以降の記事で紹介できたらと思います。

📖 参考

clientHeight, scrollHeight の説明を以下に記述しておきます。


clientHeight, scrollHeight の説明[2]

脚注
  1. https://copypet.jp/codedescription/499/ ↩︎

  2. https://ja.javascript.info/size-and-scroll#ref-96 ↩︎

Discussion