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

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

![SimpleBar logo](http://grsmto.github.io/simplebar/static/logo-b9548eb4e7099f8133fd4d039b2dff43.svg)


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

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

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


ライブラリの概要は[ドキュメント](https://github.com/Grsmto/simplebar/tree/master/packages/simplebar#simplebar--)と[デモページ](https://grsmto.github.io/simplebar/examples/)でお確かめください。本稿と同じタイトルの「[JavaScript + SimpleBar: スクロールバーのスタイルをカスタマイズする](https://qiita.com/FumioNonaka/items/40d9b2a196a8a2ee52bd)」でつくったつぎの作例は、標準のJavaScriptコードでSimpleBarのスタイルを割り当てました。「Left Column」にマウスポインタを重ねると、グラデーションのスクロールバーが現れます。今回のお題は、React用の[SimplebarReact](https://github.com/Grsmto/simplebar/tree/master/packages/simplebar-react#simplebarreact)で同じサンプルをつくることです。

<p class="codepen" data-height="360" data-theme-id="light" data-default-tab="html,result" data-user="FumioNonaka" data-slug-hash="ZEWzQwK" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="JavaScript + SimpleBar: Customizing scrollbar style">
  <span>See the Pen <a href="https://codepen.io/FumioNonaka/pen/ZEWzQwK">
  JavaScript + SimpleBar: Customizing scrollbar style</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

本稿の作例は、[Create React App](https://create-react-app.dev)のひな形アプリケーションをもとにつくります。ひな形のつくり方については「[Reactアプリケーションのひな形をつくる](http://www.fumiononaka.com/Business/html5/FN1912002.html#sec01)」をお読みください。


# インストール
まず、SimpleBarをインストールします(「[Installation](https://github.com/Grsmto/simplebar/tree/master/packages/simplebar#installation)」)。SimplebarReactもSimpleBarのCSSを使うからです。npmまたはyarnでインストールしてください。

```terminal
npm install simplebar --save
```

```terminal
yarn add simplebar
```

つぎに、SimplebarReactのインストールです。やはり、npmまたはyarnで行います(「[Installation](https://github.com/Grsmto/simplebar/tree/master/packages/simplebar-react#installation)」)。

```terminal
npm install simplebar-react --save
```

```terminal
yarn add simplebar-react
```


# 基本となるページの組み立て
ページを構成する要素は大きく3つ、ヘッダと左カラム、そしてメインコンテンツです(図001)。また、[Bootstrap 4.5]()を用いました。ただし、本稿ではCSSの説明は基本的に省き、SimpleBarの扱いに関わる定めだけ解説することにします。確かめたい方は、[最後](https://qiita.com/drafts#cssでスクロールバーのスタイルを変える)に掲げるCodeSandboxのサンプルまたはGithubのソースをご覧ください。

#### 図001■ヘッダと左カラムにメインコンテンツで組み立てられたページ
![2007001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/aa823315-48ee-c9a1-c663-46bb6d09e5e6.png)

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

SimplebarReactのJavaScriptライブラリを`import`するのは、スクロールバーをカスタマイズする左カラムのモジュール(`src/components/LeftColumn.js`)です。なお、リストの連番項目は、メソッド[`Array.from()`](https://qiita.com/FumioNonaka/items/4ff7b392bbd8232a00ba#arrayfromメソッド)と[`Array.prototype.map()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map)でつくりました。興味のある方は「[ECMAScript 6のArrayに関わる構文を試す](https://qiita.com/FumioNonaka/items/4ff7b392bbd8232a00ba)」をお読みください。

```react:src/App.js
import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import "simplebar/dist/simplebar.min.css";
import "./App.css";
import Header from "./components/Header";
import MainContents from "./components/MainContents";
import LeftColumn from "./components/LeftColumn";

function App() {
  return (
    <div className="App">
      <Header />
      <div className="container-fluid d-flex px-0">
        <LeftColumn />
        <MainContents />
      </div>
    </div>
  );
}

export default App;
```

```react:src/components/Header.js
import React from "react";

const Header = () => {
  return (
    <header id="header" className="text-white bg-primary w-100 p-2 d-flex">
      <h1>Header</h1>
    </header>
  );
};

export default Header;
```

```react:src/components/LeftColumn.js
import React from "react";

const LeftColumn = () => {
  return (
    <div id="left-column" className="bg-light p-2">
      <h3>Left column</h3>
      <ul id="list" className="pl-4">
        {Array.from(new Array(20), (_, index) => (
          <li key={index}>item {String(index + 1).padStart(2, 0)}</li>
        ))}
      </ul>
    </div>
  );
};

export default LeftColumn;
```

```react:src/components/MainContents.js
import React from "react";

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

export default MainContents;
```


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

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

#### 図002■ページ上部をヘッダが覆ってしまう
![2007001_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/3b5c75d1-aab2-8d52-a5ca-98ae78e50c3b.png)

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

要素の高さは[`element.clientHeight`](https://developer.mozilla.org/docs/Web/API/Element/clientHeight)で得られます。要素を参照するフックは[`useRef`](https://ja.reactjs.org/docs/hooks-reference.html#useref)です(`src/App.js`)。参照(`header`)は、プロパティでヘッダのコンポーネント(`src/components/Header.js`)に渡します。ただし、`element.clientHeight`は、読み取り専用プロパティであることにご注意ください。高さの設定には、要素(`<div>`)の[`style`](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/style)属性を用います。

```react:src/App.js
// import React from "react";
import React, { useEffect, useRef, useState } from "react";


function App() {
  const [headerHeight, setHeaderHeight] = useState(0);
  const header = useRef(null);
  useEffect(() => {
    const _header = header.current;
    const setLayout = () => {
      setHeaderHeight(_header.clientHeight);
    }
    window.addEventListener("resize", setLayout);
    setLayout();
  }, []);
  return (
    // <div className="App">
    <div className="App" style={{ paddingTop: headerHeight }}>
      {/* <Header /> */}
      <Header headerRef={header} />

    </div>
  );
}
```

```react:src/components/Header.js
// const Header = () => {
const Header = ({ headerRef }) => {
  return (
    <header
      id="header"
      ref={headerRef}

    >
      <h1>Header</h1>
    </header>
  );
};
```


# SimpleBarを組み込む
SimpleBarを使う要素には、[`overflow`プロパティに`auto`](https://github.com/Grsmto/simplebar/tree/master/packages/simplebar#simplebar--)を与えてください(`src/App.css`)。そのうえで、SimpleBarを設定する要素は`<SimpleBar>`に置き替えます(`src/components/LeftColumn.js`)。

```css:src/App.css
#left-column {
  overflow: auto;
}
```

```react:src/components/LeftColumn.js
import SimpleBar from "simplebar-react";

const LeftColumn = () => {
  return (
    // <div id="left-column" className="bg-light p-2">
    <SimpleBar id="left-column" className="bg-light p-2">

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

そして、スクロールバーを表示するには、要素に高さを定めなければなりません。

```css:src/App.css
#left-column {
  overflow: auto;
  height: 400px;  /* 高さを定める */
}
```

とはいえ、高さを決め打ちは避けたいところです。すでに、ヘッダの高さはとれるのですから、ブラウザウィンドウのビューポートの高さ([`window.innerHeight`](https://developer.mozilla.org/docs/Web/API/Window/innerHeight))から差し引けば、左カラムの高さ(`leftColumnHeight`)は求まります。

```react:src/App.js
function App() {

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

  useEffect(() => {

    const setLayout = () => {

      setLeftColumnHeight(window.innerHeight - _header.clientHeight);
    };

  }, []);
  return (
    <div className="App" style={{ paddingTop: headerHeight }}>

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

      </div>
    </div>
  );
}
```

```react:src/components/LeftColumn.js
// const LeftColumn = () => {
const LeftColumn = ({ 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>
  );
};
```

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

#### 図003■SimpleBarのスクロールバーが自動表示される
![2008002_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/eeb9dc4f-e277-8542-d924-860c1fe30bb7.png)


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

#### 図004■ページを下にスクロールすると左カラムがヘッダにかぶる
![2008002_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/bec042b8-6ef2-9e78-9d20-7ecbe6d744a1.png)

左カラム(`src/components/LeftColumn.js`)の垂直位置は固定しなければなりません。やり方は、前述「[ヘッダを上部に固定する](https://qiita.com/drafts/097d0ff8d73bf7e59867/edit#ヘッダを上部に固定する)」と同じです。ただ、CSS(`src/App.css`)でなく、`style`属性で定めることにしました。

```react:src/App.js
function App() {

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

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

        />

      </div>
    </div>
  );
}
```


```react:src/components/LeftColumn.js
// const LeftColumn = ({ leftColumnHeight }) => {
const LeftColumn = ({ headerHeight, leftColumnHeight }) => {
  return (
    <SimpleBar

      style={{
        position: "fixed",
        top: headerHeight,

      }}
    >

    </SimpleBar>
  );
};
```

もちろん、前述「ヘッダを上部に固定する」と同じように、左カラムがメインコンテンツにかぶってしまいます(図005)。

#### 図005■メインコンテンツが左カラムに隠れてしまう
![2008002_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ba3a2413-d927-e35b-390d-ea733862157b.png)

「ヘッダを上部に固定する」と同じ考え方で、メインコンテンツの左端をカラムの幅だけ右に寄せればよいはずです。けれど、つぎのコードではメインコンテンツの位置がまったく動きません。

```react:src/App.js
function App() {
  const [leftColumnWidth, setLeftColumnWidth] = useState(0);

  const leftColumn = useRef(null);
  useEffect(() => {

    const _leftColumn = leftColumn.current;
    const setLayout = () => {

      setLeftColumnWidth(_leftColumn.clientWidth);

    };

  }, [leftColumn]);
  return (
    <div className="App" style={{ paddingTop: headerHeight }}>

      <div className="container-fluid d-flex px-0">
        <LeftColumn
          leftColumnRef={leftColumn}

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

```react:src/components/MainContents.js
// const MainContents = () => {
const MainContents = ({ leftColumnWidth }) => {
  return (
    // <main className="px-4 py-2">
    <main className="px-4 py-2" style={{ marginLeft: leftColumnWidth }}>

    </main>
  );
};
```

```react:src/components/LeftColumn.js
// const LeftColumn = ({ headerHeight, leftColumnHeight }) => {
const LeftColumn = ({headerHeight, leftColumnHeight, leftColumnRef }) => {
  return (
    <SimpleBar

      ref={leftColumnRef}

    >

    </SimpleBar>
  );
};
```


# SimpleBarコンポーネントをラップする

調べてみると、`SimpleBar`コンポーネントの`clientWidth`プロパティ値が0です。SimpleBarは、あくまでスクロールバーのスタイルを整えるためのラッパーだからでしょう。

そこで、`Simplebar`コンポーネントをつぎのように`<div>`要素で包み、スクロールバーに用いる以外の属性はすべてこの要素に移します。こうすることで、カラムの要素の幅(`clientWidth`)が正しく得られるのです。メインコンテンツの左端は、カラムの右端に揃います。

```react:src/components/LeftColumn.js
const LeftColumn = ({ headerHeight, leftColumnHeight, leftColumnRef }) => {
  return (
    <div
      id="left-column"
      ref={leftColumnRef}
      className="bg-light p-2"
      style={{
        position: "fixed",
        top: headerHeight,
      }}
    >
      <SimpleBar
        /* id="left-column"
        ref={leftColumnRef}
        className="bg-light p-2" */
        style={{
          /* position: "fixed",
          top: headerHeight, */
          height: leftColumnHeight,
        }}
      >

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


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

SimpleBarのスクロールバーのスタイルは、CSSにより定められています。つまり、[見栄えがCSSで変えられる](https://github.com/Grsmto/simplebar/tree/master/packages/simplebar#styling)ということです。ここでは、スクロールさせるスライダのカラーを、つぎのCSSでグラデーションにしてみましょう(図006)。

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

#### 図006■メインコンテンツの位置が正しく定まってスライダはグラデーションになった
[![2008002_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/49d1676b-f502-8412-8bcb-509ff15cfa89.png)
\>\> CodeSandboxへ](https://codesandbox.io/s/react-simplebar-customizing-scrollbar-style-k7u0n)

冒頭の標準JavaScriptのサンプルと同じページをSimplebarReactでつくり、[CodeSandbox](https://codesandbox.io/s/react-simplebar-customizing-scrollbar-style-k7u0n)に掲げました。また、[Github](https://github.com/FumioNonaka/react-simplebar)でもソースをご覧いただけます。