SimpleBarはスクロールバーをカスタマイズするライブラリです。スクロールバーを独自につくるのではなく、CSSのスタイルを割り当てるので、おかしな挙動は起こらず、ネイティブなスクロールのパフォーマンスが保たれます。あくまで、スクロールバーの見栄えを変えるだけです。
デザインは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■ヘッダと左カラムにメインコンテンツで組み立てられたページ
以下のコードは、アプリケーション(src/App.tsx
)に、それぞれヘッダ(src/components/Header.tsx
)と左カラム(src/components/LeftColumn.tsx
)およびメインコンテンツ(src/components/MainContents.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>
);
}
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>
);
};
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>
);
};
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)。
#header {
position: fixed;
top: 0;
}
図002■ページ上部をヘッダが覆ってしまう
本文の要素のpadding
またはmargin
は、ヘッダの高さ分下げなければならないのです。もっとも、高さはウィンドウ幅やレスポンシブの設定によって変わるかもしれません。動的に定めるべきでしょう。
要素の高さはelement.clientHeight
で得られます。要素参照のためのrefオブジェクトをつくるフックがuseRef
です。ロジックは新たに定めるカスタムフックのモジュールsrc/hooks/useLayout.ts
に切り出しましょう。フックの戻り値オブジェクトに含むのは、ヘッダの高さ(headerHeight
)とrefオブジェクト(headerRef
)です。
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
プロパティです。
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
)は、ヘッダの高さが動的に得られるのです。
// 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
)です。
#left-column {
overflow: auto;
}
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
することは忘れないでください。
export const useLayout = () => {
const [leftColumnHeight, setLeftColumnHeight] = useState(0);
const setLayout = useCallback(() => {
if (header) {
setLeftColumnHeight(window.innerHeight - header.clientHeight);
}
}, []);
return {
leftColumnHeight,
};
};
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のスクロールバーが自動表示される
ページ全体をスクロールしたときの不具合を直す
まだ少し、不具合が残っています。ページ全体を下にスクロールしたとき、左カラムがせり上がって、ヘッダにかぶってしまうことです(図004)。
図004■ページ全体を下にスクロールすると左カラムがヘッダにかぶる
左カラム(LeftColumn
)の垂直位置は固定しなければなりません。考え方は、前述「ヘッダを上部に固定する」と同じです。ただし、ヘッダの高さ(headerHeight
)はカスタムフック(useLayout
)により動的に算出されます。したがって、コンポーネントにはプロパティとして渡すのです。
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
属性で固定します。position
はfixed
、top
に与えるのがプロパティとして受け取ったヘッダの高さ(headerHeight
)です。
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■メインコンテンツが左カラムに隠れてしまう
今度は、メインコンテンツの左端位置を左カラムの幅に固定しなければなりません。カスタムフック(useLayout
)は、幅の状態変数(leftColumnWidth
)と左カラムに渡すrefオブジェクト(leftColumnRef
)をつくって戻り値に加えます。
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
)に渡せばよさそうです。
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>
)に移してください。
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>
です。
// 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でお確かめください。
.simplebar-scrollbar::before {
background: linear-gradient(darkblue, skyblue);
}