LoginSignup
3
2

More than 1 year has passed since last update.

React + TypeScript: SimpleBarでスクロールバーのスタイルをカスタマイズする

Last updated at Posted at 2022-09-27

SimpleBarはスクロールバーをカスタマイズするライブラリです。スクロールバーを独自につくるのではなく、CSSのスタイルを割り当てるので、おかしな挙動は起こらず、ネイティブなスクロールのパフォーマンスが保たれます。あくまで、スクロールバーの見栄えを変えるだけです。

SimpleBar logo

デザインはCSSで定める

SimpleBarは純粋なCSSでスクロールバーのスタイルを定めます。CSSで与えられるスタイルでさえあれば、自由にカスタマイズできるということです。また、macOSとWindowsで同じ見た目になるのも大きな魅力といえます。

軽量なライブラリ

6KBのとても軽いライブラリです。JavaScriptはスクロールの動きそのものには触れません。ネイティブな動きとパフォーマンスが得られます。

モダンブラウザをサポート

ChromeとFirefox、Safariなどのモダンブラウザに加え、Internet Explorer 11をサポートします。

ライブラリの概要はドキュメントデモページでお確かめください。別稿の「JavaScript + SimpleBar: スクロールバーのスタイルをカスタマイズする」でつくったつぎの作例は、標準のJavaScriptコードでSimpleBarのスタイルを割り当てました。「Left Column」にマウスポインタを重ねると、グラデーションのスクロールバーが現れます。今回のお題は、React用のSimplebarReactで同じサンプルをつくることです。

See the Pen JavaScript + SimpleBar: Customizing scrollbar style by Fumio Nonaka (@FumioNonaka) on CodePen.

ひな形プロジェクトの用意

本稿でつくるのは、つぎのサンプル001の作例です。CodeSandboxのテンプレート「React TypeScript」をひな形にしました。左カラム(「Left column」)のスクロールに、SimpleBarを使っています。

サンプル001■React + TypeScript: Customize scrollbar style with SimpleBar - CodeSandbox

なお、本稿ではCSSについては、SimpleBarを使うための最小限の定めしか触れません。具体的な記述は、サンプル001のsrc/styles.cssをご覧ください。

CodeSandboxを使う

CodeSandboxのTemplatesから「React TypeScript」を開いてください。前掲サンプル001の依存(Dependencies)に加えたのは、つぎの5つです。

Dependencies

  • bootstrap
  • react
  • react-dom
  • react-scripts
  • simplebar-react

Create React Appを使う

ローカルにReact + TypeScriptのひな型プロジェクトをつくる場合は、Create React Appを使うとよいでしょう。手順については「Create React AppでTypeScriptが加わったひな形アプリケーションをつくる」をお読みください。SimplebarReactは、npmもしくはYarnでインストールします。

install simplebar-react --save
yarn add simplebar-react

なお、本稿作例と同じスタイルにしたい場合には、Bootstrapも併せてインストールしてください。

基本となるページの組み立て

ページを構成する要素は大きく3つ、ヘッダと左カラム、そしてメインコンテンツです(図001)。また、作例にはBootstrap 5を用いました。ただし、本稿ではCSSの説明は基本的に省き、SimpleBarの扱いに関わる定めに絞って解説することにします。

図001■ヘッダと左カラムにメインコンテンツで組み立てられたページ

2007001_002.png

以下のコードは、アプリケーション(src/App.tsx)に、それぞれヘッダ(src/components/Header.tsx)と左カラム(src/components/LeftColumn.tsx)およびメインコンテンツ(src/components/MainContents.tsx)を静的にレイアウトしたモジュールの中身です。

src/App.tsx
import 'bootstrap/dist/css/bootstrap.min.css';
import { Header } from './components/Header';
import { MainContents } from './components/MainContents';
import { LeftColumn } from './components/LeftColumn';
import './styles.css';

export default function App() {
	return (
		<div className="App">
			<Header />
			<div className="container-fluid d-flex px-0">
				<LeftColumn />
				<MainContents />
			</div>
		</div>
	);
}
src/components/Header.tsx
import type { FC } from 'react';

export const Header: FC = () => {
	return (
		<header id="header" className="text-white bg-primary w-100 p-2 d-flex">
			<h1>Header</h1>
		</header>
	);
};
src/components/LeftColumn.tsx
import type { FC } from 'react';

const listItems = (count: number) =>
	Array.from(new Array(count), (_, index) => (
		<li key={index}>item {String(index + 1).padStart(2, '0')}</li>
	));
export const LeftColumn: FC = () => {
	return (
		<div id="left-column" className="bg-light p-2">
			<h3>Left column</h3>
			<ul id="list" className="pl-4">
				{listItems(50)}
			</ul>
		</div>
	);
};
src/components/MainContents.tsx
import type { FC } from 'react';

export const MainContents: FC = () => {
	return (
		<main className="px-4 py-2">
			<h2>Main contents</h2>
			<p>
				{/* ...[中略]... */}
			</p>
		</main>
	);
};

ヘッダを上部に固定する

まずは、ヘッダをページ上部に固定するCSSの設定です(src/styles.css)。位置はpositionプロパティにfixedを与えて固定します。具体的な置き場所は最上部なのでtop: 0です。すると、<body>要素の領域に含まれなくなるので、そのままではページの上部がかぶって隠れてしまいます(図003)。

src/styles.css
#header {
	position: fixed;
	top: 0;
}

図002■ページ上部をヘッダが覆ってしまう

2007001_003.png

本文の要素のpaddingまたはmarginは、ヘッダの高さ分下げなければならないのです。もっとも、高さはウィンドウ幅やレスポンシブの設定によって変わるかもしれません。動的に定めるべきでしょう。

要素の高さはelement.clientHeightで得られます。要素参照のためのrefオブジェクトをつくるフックがuseRefです。ロジックは新たに定めるカスタムフックのモジュールsrc/hooks/useLayout.tsに切り出しましょう。フックの戻り値オブジェクトに含むのは、ヘッダの高さ(headerHeight)とrefオブジェクト(headerRef)です。

src/hooks/useLayout.ts
import { useCallback, useEffect, useRef, useState } from 'react';

export const useLayout = () => {
	const [headerHeight, setHeaderHeight] = useState(0);
	const headerRef = useRef<HTMLElement>(null);
	const setLayout = useCallback(() => {
		const header = headerRef.current;
		if (header) {
			setHeaderHeight(header.clientHeight);
		}
	}, []);
	useEffect(() => {
		setLayout();
		window.addEventListener('resize', setLayout);
		return () => window.removeEventListener('resize', setLayout);
	}, []);
	return {
		headerHeight,
		headerRef
	};
};

refオブジェクトの参照(headerRef)は、モジュールsrc/App.tsxからプロパティとしてヘッダのコンポーネント(Header)に渡します。ただし、element.clientHeightは、読み取り専用プロパティであることにご注意ください。高さの設定に用いるのは、要素(<div>)のstyle属性に与えたpaddingTopプロパティです。

src/App.tsx
import { useLayout } from './hooks/useLayout';

export default function App() {
	const { headerHeight, headerRef } = useLayout();
	return (
		// <div className="App">
		<div className="App" style={{ paddingTop: headerHeight }}>
			{/* <Header /> */}
			<Header headerRef={headerRef} />

		</div>
	);
}

Headerコンポーネントいついては、引数に受け取ったrefオブジェクト(headerRef)を最上位要素(<header>)のref属性に加えてください。これでカスタムフック(useLayout)は、ヘッダの高さが動的に得られるのです。

src/components/Header.tsx
// import type { FC } from 'react';
import type { FC, RefObject } from 'react';

type Props = {
	headerRef: RefObject<HTMLElement>;
};
// export const Header: FC = () => {
export const Header: FC<Props> = ({ headerRef }) => {
	return (
		// <header id="header" className="text-white bg-primary w-100 p-2 d-flex">
		<header
			id="header"
			ref={headerRef}
			className="text-white bg-primary w-100 p-2 d-flex"
		>

		</header>
	);
};

SimpleBarを組み込む

SimpleBarを組み込む要素(id属性left-column)には、overflowプロパティにautoを与えてください(src/styles.css)。そのうえで、SimpleBarを設定する要素は<SimpleBar>に置き替えます(src/components/LeftColumn.tsx)。そして、スクロールバーを表示するには、要素に高さ(height)を定めなければなりません。渡されるのは、このあとカスタムフック(useLayout)が求める左カラムの高さ(leftColumnHeight)です。

src/styles.css
#left-column {
	overflow: auto;

}
src/components/LeftColumn.tsx
import SimpleBar from 'simplebar-react';

type Props = {
	leftColumnHeight: number;
};

// export const LeftColumn: FC = () => {
export const LeftColumn: FC<Props> = ({ leftColumnHeight }) => {
	return (
		// <div id="left-column" className="bg-light p-2">
		<SimpleBar
			id="left-column"
			className="bg-light p-2"
			style={{
				height: leftColumnHeight
			}}
		>

			{/* </div> */}
		</SimpleBar>
	);
};

カスタムフック(useLayout)は、すでにヘッダの高さは知っています。すると、ブラウザウィンドウのビューポートの高さ(window.innerHeight)から差し引けば、左カラムの高さ(leftColumnHeight)は求まるのです。なお、ルートモジュール(src/App.tsx)でSimpleBarのCSS(simplebar-react/dist/simplebar.min.css)をimportすることは忘れないでください。

src/hooks/useLayout.ts
export const useLayout = () => {

	const [leftColumnHeight, setLeftColumnHeight] = useState(0);

	const setLayout = useCallback(() => {

		if (header) {

			setLeftColumnHeight(window.innerHeight - header.clientHeight);
		}
	}, []);

	return {

		leftColumnHeight,
	};
};
src/App.tsx
import 'simplebar-react/dist/simplebar.min.css';

export default function App() {
	// const { headerHeight, headerRef } = useLayout();
	const { headerHeight, headerRef, leftColumnHeight } = useLayout();
	return (
		<div className="App" style={{ paddingTop: headerHeight }}>

			<div className="container-fluid d-flex px-0">
				{/* <LeftColumn /> */}
				<LeftColumn leftColumnHeight={leftColumnHeight} />

			</div>
		</div>
	);
}

これで、ウィンドウに合わせて左カラムの高さが定まり、スクロールバーは自動表示されるようになりました(図003)。

図003■SimpleBarのスクロールバーが自動表示される

2008002_001.png

ページ全体をスクロールしたときの不具合を直す

まだ少し、不具合が残っています。ページ全体を下にスクロールしたとき、左カラムがせり上がって、ヘッダにかぶってしまうことです(図004)。

図004■ページ全体を下にスクロールすると左カラムがヘッダにかぶる

2008002_002.png

左カラム(LeftColumn)の垂直位置は固定しなければなりません。考え方は、前述「ヘッダを上部に固定する」と同じです。ただし、ヘッダの高さ(headerHeight)はカスタムフック(useLayout)により動的に算出されます。したがって、コンポーネントにはプロパティとして渡すのです。

src/App.tsx
export default function App() {

	return (
		<div className="App" style={{ paddingTop: headerHeight }}>

			<div className="container-fluid d-flex px-0">
				{/* <LeftColumn leftColumnHeight={leftColumnHeight} /> */}
				<LeftColumn
					headerHeight={headerHeight}
					leftColumnHeight={leftColumnHeight}
				/>

			</div>
		</div>
	);
}

左カラムコンポーネント(LeftColumn)の垂直位置は、要素のstyle属性で固定します。positionfixedtopに与えるのがプロパティとして受け取ったヘッダの高さ(headerHeight)です。

src/components/LeftColumn.tsx
type Props = {
	headerHeight: number;

};

// export const LeftColumn: FC<Props> = ({ leftColumnHeight }) => {
export const LeftColumn: FC<Props> = ({ headerHeight, leftColumnHeight }) => {
	return (
		<SimpleBar

			style={{

				position: "fixed",
				top: headerHeight
			}}
		>

		</SimpleBar>
	);
};

けれど、これだけではまだ足りません。前述「ヘッダを上部に固定する」と同じように、左カラムがメインコンテンツにかぶってしまうからです(図005)。

図005■メインコンテンツが左カラムに隠れてしまう

2008002_003.png

今度は、メインコンテンツの左端位置を左カラムの幅に固定しなければなりません。カスタムフック(useLayout)は、幅の状態変数(leftColumnWidth)と左カラムに渡すrefオブジェクト(leftColumnRef)をつくって戻り値に加えます。

src/hooks/useLayout.ts
export const useLayout = () => {
	const [leftColumnWidth, setLeftColumnWidth] = useState(0);

	const leftColumnRef = useRef<HTMLDivElement>(null);
	const setLayout = useCallback(() => {

		const leftColumn = leftColumnRef.current;

		if (leftColumn) setLeftColumnWidth(leftColumn.clientWidth);
	}, []);

	return {

		leftColumnRef,
		leftColumnWidth
	};
};

ルートコンポーネント(App)は、カスタムフックから取り出したrefオブジェクト(leftColumnRef)を左カラム(LeftColumn)に、左カラムの幅(leftColumnWidth)はメインコンテンツ(MainContents)に渡せばよさそうです。

src/App.tsx
export default function App() {
	const {

		leftColumnRef,
		leftColumnWidth
	} = useLayout();
	return (
		<div className="App" style={{ paddingTop: headerHeight }}>

			<div className="container-fluid d-flex px-0">
				<LeftColumn

					leftColumnRef={leftColumnRef}

				/>
				<MainContents leftColumnWidth={leftColumnWidth} />
			</div>
		</div>
	);
}

ところが、ここで問題が生じます。SimpleBarコンポーネントは、clientWidthプロパティをもちません(後述)。つまり、要素の幅が得られないのです。対応するためには、改めて<SimpleBar>コンポーネントを<div>要素で包みます。そのうえで、styleプロパティのheightの定め以外、属性はすべて親の要素(<div>)に移してください。

src/components/LeftColumn.tsx
type Props = {

	leftColumnRef: RefObject<HTMLDivElement>;
};

export const LeftColumn: FC<Props> = ({

	leftColumnRef
}) => {
	return (
		<div
			id="left-column"
			ref={leftColumnRef}
			className="bg-light p-2"
			style={{
				position: 'fixed',
				top: headerHeight
			}}
		>
			<SimpleBar
				/* id="left-column"
				className="bg-light p-2" */
				style={{
					height: leftColumnHeight
					/* position: 'fixed',
					top: headerHeight */
				}}
			>

			</SimpleBar>
		</div>
	);
};

SimpleBarコンポーネントにはclientWidthプロパティがない

SimpleBarコンポーネントにrefオブジェクトを与えようとすると、型づけはRefObject<SimpleBar>です。

src/hooks/useLayout.ts
// const leftColumnRef = useRef<HTMLDivElement>(null);
const leftColumnRef = useRef<SimpleBar>(null);

このrefオブジェクトからclientWidthプロパティを参照しようとすると、つぎのような警告が示されて取得できません。

Property 'clientWidth' does not exist on type 'SimpleBar'.

CSSでスクロールバーのスタイルを変える

SimpleBarのスクロールバーのスタイルは、CSSにより定められています。つまり、見栄えがCSSで変えられるということです。ここでは、スクロールさせるスライダのカラーを、つぎのCSSでグラデーションにしてみましょう(図006)。書き上がった作例のコードと動きは、冒頭に掲げたCodeSandboxのサンプル001でお確かめください。

src/styles.css
.simplebar-scrollbar::before {
	background: linear-gradient(darkblue, skyblue);
}

図006■メインコンテンツの位置が正しく定まってスライダはグラデーションになった

2008002_004.png

3
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
3
2