ある要素の上にマウスをのせた時に表示されるプチ情報をTooltipといいます。表示される情報はユーザが実行する操作説明や難解/業界特有の用語の補足説明が大半を占めています。Tooltipをうまく活用することでユーザビリティを向上につながるためさまざまなアプリケーションで利用されています。

Reactを利用してTooltipを実装する場合、React Hookなどの基本機能(useState, useRef, useEffect)を利用する箇所があります。本文書ではTooltipsの実装方法だけではなくTooltipsの実装を通してReactの基礎知識を深めていくことを目的としています。react-tooltipというライブラリもありますが本文書ではライブラリは一切利用しません。

React環境の構築

Reactのプロジェクトを作成するためにはnodeがインストールされている必要があります。nodeがインストールされていない場合はnodeインストール後にReactのプロジェクトの作成を行なってください。プロジェクト名にreact-tooltipとつけていますが任意の名前をつけて実行してください。


 % npx create-react-app react-tooltip

プロジェクト作成後、srcフォルダにあるApp.jsファイルを下記のように記述します。


function App() {
  return (
    <div
      style={{
        width: '500px',
        margin: '0 auto',
      }}
    >
      <h1>Reactで初めてのTooltip実装</h1>
      <div>
        <button>Tooltip</button>
      </div>
    </div>
  );
}
export default App;

App.jsファイルを更新後は、npm run startコマンドを実行してアプリケーションの起動を行なってください。

ブラウザで確認すると以下の画面が表示されます。

初期画面
初期画面

マウスのhoverによるtooltipの表示

Tooltipコンポーネントの作成

srcフォルダの下にcomponentsフォルダを作成し、Tooltip.jsファイルとCSSに利用するTooltip.cssファイルを作成します。Tooltip.jsではimportでTooltip.cssをインポートします。

Tooltip.jsファイルでは、propsを使って親コンポーネントからchildrenを受け取り表示させています。


import './Tooltip.css';

const Tooltip = ({ children }) => {
  return (
    <div className="container">
      <div>{children}</div>
    </div>
  );
};

export default Tooltip;

classNameで設定したcontainerクラスはTooltip.cssファイルに記述します。後ほど表示させるTooltipにはpositionのabsoluteを利用するためtooltip要素の親要素であるdiv要素にはpositionのrelativeを設定しています。


.container {
  position: relative;
  display: inline-block;
}

App.jsファイルで作成したTooltipコンポーネントをimportしてボタン要素をwrap(包む)します。


import Tooltip from './components/Tooltip';
function App() {
  return (
    <div
      style={{
        width: '500px',
        margin: '0 auto',
      }}
    >
      <h1>Reactで初めてのTooltip実装</h1>
      <div>
        <Tooltip>
          <button>Tooltip</button>
        </Tooltip>
      </div>
    </div>
  );
}
export default App;

Tooltipを追加しましたが、ブラウザ上での表示に変化はありません。デベロッパーツールを利用して要素を確認するとcontainerクラスなどが設定されていることが確認することができます。

propsでtooltipの表示内容を渡す

Tooltipコンポーネントを再利用するためにTooltipコンポーネントの中でtooltipに表示させる内容を記述するのではなくコンポーネントの外側からpropsを通してtooltipに表示させたい内容を設定します。そのためにcontentという名前のpropsを追加します。


<Tooltip content="Tooltipに表示させたい内容をここに記述します">
  <button>Tooltip</button>
</Tooltip>

Tooltipコンポーネントでcontent propsを受け取り表示させます。


import './Tooltip.css';

const Tooltip = ({ children, content }) => {
  return (
    <div className="container">
      <div>{children}</div>
      <div>{content}</div>
    </div>
  );
};

export default Tooltip;

受け取ったcontent propsが表示されていることを確認します。

contentの内容を表示
contentの内容を表示

contentの内容を表示することができましたがこのままでの設定ではTooltipボタンと関連を持たないまま常時表示されていることになります。React HookのuseStateを利用して表示の切り替え機能を追加します。

useStateの設定

useStateを使って新たに変数showを追加します。trueとfalseの値を持つことができtrueの場合にtooltipの内容を表示し、falseの場合に非表示にします。


import { useState } from 'react';
import './Tooltip.css';

const Tooltip = ({ children, content }) => {
  const [show, setShow] = useState(false);
  return (
    <div className="container">
      <div>{children}</div>
      {show && <div>{content}</div>}
    </div>
  );
};

export default Tooltip;

showの初期値はfalseなのでブラウザで確認すると先ほどまで表示されていたcontent propsの内容が非表示になります。

showの値がfalseの場合
showの値がfalseの場合

ユーザからのインタラクションを元に非表示のcontentを表示させる必要があります。ボタン要素の上にマウスがある場合に表示、マウスが離れた場合に非表示になるようにmouseEnter, mouseLeaveイベントを利用して表示・非表示を切り替えられるようにします。


import { useState } from 'react';
import './Tooltip.css';

const Tooltip = ({ children, content }) => {
  const [show, setShow] = useState(false);
  return (
    <div className="container">
      <div
        onMouseEnter={() => setShow(true)}
        onMouseLeave={() => setShow(false)}
      >
        {children}
      </div>
      {show && <div>{content}</div>}
    </div>
  );
};

export default Tooltip;

ここまでの設定でボタンの上にマウスの乗せるとcontent propsの内容が表示されマウスを外すと非表示になる機能を実装することができました。

contentの内容を表示
contentの内容を表示

Tooltipの表示場所の設定

以下のようにTooltipコンポーネントの下にもコンテンツを持つ要素が続く場合どうのような表示になるか確認します。


<h1>Reactで初めてのTooltip実装</h1>
<div>
  <Tooltip content="Tooltipに表示させたい内容をここに記述します">
    <button>Tooltip</button>
  </Tooltip>
  <div>ここから文書は続きます。</div>
</div>

コンテンツを追加したのでTooltipボタンの下に追加したコンテンツが表示されます。

Tooltipコンポーネントの下にコンテンツ追加
Tooltipコンポーネントの下にコンテンツ追加

Tooltipボタンにマウスをのせるとcontent propsの内容が表示されるだけではなく先ほど追加したコンテンツも下に下がることになり、contentの表示・非表示でページ全体のレイアウトが変化します。

tooltipの内容を表示
tooltipの内容を表示

この問題を避けるため表示させるtooltipの要素のpositionにabsoluteを設定します。Tooltip.cssファイルにrightクラスを追加します。


.right {
  position: absolute;
  min-width: 200px;
  z-index: 10;
  top: 50%;
  left: calc(100% + 5px);
  transform: translateY(-50%);
  background-color: black;
  color: white;
  padding: 0.5em 1em;
  border-radius: 5px;
}

rightクラスの設定のポイントはtop, left, translateYのパーセント設定です。それぞれの値がどこの場所に影響を与えているのか言葉では説明が難しい下記の図を参考にしてください。

leftクラスのCSSの位置
leftクラスのCSSの位置
一般的なtooltipでは吹き出しのように四角の1辺に三角がついています。必要であればCSSを追加してください。本書ではCSSの設定に注目していないので割愛しています。

設定したrightクラスをtooltipの内容が表示されるdiv要素に設定します。


{show && <div className="right">{content}</div>}

設定後ブラウザで確認すると下記のように表示されます。Tooltipボタンの上にマウスをのせると右側にtooltipの内容が表示できるようになりました。

tooltipのleftクラス設定後
tooltipのleftクラス設定後

tooltipの表示場所の指定

Tooltipコンポーネントをさらに汎用的にするためにtooltipの位置の設定をtooltipに表示させる内容と同様にpropsを利用して指定します。

propsのlocationを追加して値をrightとします。


<Tooltip
  content="Tooltipに表示させたい内容をここに記述します。"
  location="right"
>
  <button>Tooltip</button>
</Tooltip>

Tooltip.jsファイルではApp.jsから渡されたlocation propsを受け取ってそのままclassNameの値に設定します。


import { useState } from 'react';
import './Tooltip.css';

const Tooltip = ({ children, content, location }) => {
  const [show, setShow] = useState(false);
  return (
    <div className="container">
      <div
        onMouseEnter={() => setShow(true)}
        onMouseLeave={() => setShow(false)}
      >
        {children}
      </div>
      {show && <div className={location}>{content}</div>}
    </div>
  );
};

export default Tooltip;

locationの値にrightを設定しているので先ほどの動作と変わらずTooltipにマウスをのせると右側にtooltipが表示されます。

次にlocationの値にleftを設定してみましょう。leftを設定してもエラーになることはありませんがleftに対応するクラスがTooltip.cssに設定されていないためTooltipボタンの下に内容が表示されます。

tooltipの内容を表示
tooltipの内容を表示

右側に表示させるためにleftクラスの追加を行いましょう。先ほどのrightクラスとほとんど設定値は同じです。leftプロパティの代わりにrightプロパティが追加しています。


.left {
  position: absolute;
  min-width: 200px;
  z-index: 10;
  top: 50%;
  right: calc(100% + 5px);
  transform: translateY(-50%);
  background-color: black;
  color: white;
  padding: 0.5em 1em;
  border-radius: 5px;
}

設定後Tooltipの上にマウスをのせると左側にtooltipが表示されます。

左側にtooltip表示
左側にtooltip表示

右と左にtooltipが表示できたので次は上と下にも表示できるようにtopクラスとbottomクラスを設定しておきましょう。


.top {
  position: absolute;
  min-width: 200px;
  z-index: 10;
  bottom: calc(100% + 5px);
  left: 50%;
  transform: translateX(-50%);
  background-color: black;
  color: white;
  padding: 0.5em 1em;
  border-radius: 5px;
}

.bottom {
  position: absolute;
  min-width: 200px;
  z-index: 10;
  top: calc(100% + 5px);
  left: 50%;
  transform: translateX(-50%);
  background-color: black;
  color: white;
  padding: 0.5em 1em;
  border-radius: 5px;
}

locationにtopを設定した場合はtooltipボタンの上部に表示されます。

上部にtooltip表示
上部にtooltip表示

locationにbottomを設定した場合はtooltipボタンの下部に表示されます。

下部にtooltip表示
下部にtooltip表示

content propsへのstyle設定

tooltipに表示させる内容にstyleを設定したい場合の動作確認を行います。下記のようにcontent propsの中でstyleを設定します。


<Tooltip
  content={
    <span style={{ color: 'red', fontWeight: 'bold' }}>
      Tooltipに表示させたい内容をここに記述します。
    </span>
  }
  location="right"
>
  <button>Tooltip</button>
</Tooltip>

ブラウザで確認すると下記のようにtooltipの内容にもstyleが反映された状態で表示されます。

content propsにstyleを設定
content propsにstyleを設定

content propsにコンポーネントを設定

App.jsファイルにContentコンポーネントを作成して、content propsにContentコンポーネントを指定した場合に問題なく表示されるか確認を行います。


import Tooltip from './components/Tooltip';

const Content = () => {
  return (
    <div>
      <h1>Contentコンポーネント</h1>
    </div>
  );
};

function App() {
  return (
    <div
      style={{
        width: '500px',
        margin: '0 auto',
      }}
    >
      <h1>Reactで初めてのTooltip実装</h1>
      <div>
        <Tooltip content={<Content />} location="right">
          <button>Tooltip</button>
        </Tooltip>
        <div>ここから文書は続きます。</div>
      </div>
    </div>
  );
}
export default App;

設定後にブラウザで確認するとContentコンポーネントで設定した内容が表示されることが確認できます。

コンポーネントをcontet propsで指定
コンポーネントをcontet propsで指定

Clickイベントによるtooltip表示

ここまでの設定ではマウスをtooltipボタン上にのせるとtooltipが表示され、マウスをtooltipボタン上から外すとtooltipが非表示になりました。ここからクリックイベントによって表示・非表示を切り替える方法も確認していきます。実装するためにReact HookのuseRef、useEffectを利用します。

ここから設定を行う方法を使えばtooltipだけではなくある要素をクリックすると表示されるメニュー表示・非表示などにも利用することができます。

onClickイベントの設定

onMouseEnvetイベントからonClickイベントに変更してonMouseLeaveイベントは削除します。


const Tooltip = ({ children, content, location }) => {
  const [show, setShow] = useState(false);
  return (
    <div className="container">
      <div onClick={() => setShow(true)}>{children}</div>
      {show && <div className={location}>{content}</div>}
    </div>
  );
};

onClickイベントに変更したのでTooltipボタンをクリックするとtooltipの内容が表示されます。

tooltipのleftクラス設定後
onClickイベント設定後

onClickボタンを押すと表示されますが非表示にする設定がないため表示したtooltipを非表示にする方法がありません。onClickボタンではshow変数をtrueにしていましたがtrue、falseを切り替えられるように次のように変更を行います。


<div onClick={() => setShow(!show)}>{children}</div>

設定後はTooltipボタンを押すと表示され、再度Tooltipボタンを押すと非表示になり、ボタンによって表示・非表示を制御することができます。

一般的なアプリケーションでは表示されたTooltip以外の領域をクリックすると非表示になるように実装されている場合が多いのでここでもTooltip以外の領域をクリックしても閉じられるように設定を行っていきます。

useRefの設定

React HookのuseRefを利用すると設定した要素に対してReactから直接アクセスすることが可能となります。

useRefを使って以下のようにTooltipコンポーネントの外側のdiv要素に対して設定を行います。


import { useState, useRef } from 'react';
import './Tooltip.css';

const Tooltip = ({ children, content, location }) => {
  const tooltipEl = useRef();
  const [show, setShow] = useState(false);
  return (
    <div className="container" ref={tooltipEl}>
      <div onClick={() => setShow(!show)}>{children}</div>
      {show && <div className={location}>{content}</div>}
    </div>
  );
};

export default Tooltip;

clickOutside関数を追加します。tooltipEl.currentにはrefを設定したdiv要素の情報が入っており、クリックした際に発生するイベントのtargetプロパティを取得しその要素がtooltipEl.currnetに含まれていない場合にsetShowでshowの値をfalseにしています。つまりtooltipの表示領域をクリックしても何も起こりませんが、それ以外の領域をクリックするとshowの値がfalseになりtooltipが非表示になります。


const clickOutside = (e) => {
  if (!tooltipEl.current.contains(e.target)) setShow(false);
};

useEffectの設定

clickOutside関数はブラウザ上のどこをクリックしても実行させる必要があるためイベントリスナーを利用します。イベントリスナーの設定はReact HookのuseEffectを利用してマウント時に1度実行します。アンマウント時に登録したイベントリスナーを削除できるようにremoveEventListenerの設定を行っています。


useEffect(() => {
  document.addEventListener('click', clickOutside);
  return () => {
    document.removeEventListener('click', clickOutside);
  };
}, []);

設定が完了したらTooltipボタンをクリックしてtooltipを表示してください。

tooltipのleftクラス設定後
tooltipの表示

表示されたらまずtooltipの内容が表示されている領域をクリックしてください。クリックしても何も起こりません。次にtooltip以外の領域をどこでもいいのでクリックしてください。tooltipが非表示になるはずです。

このようにReact HookのuseRefとuseEffectを利用することでtooltip以外の領域をクリックすると表示されているtooltipを非表示にできる機能を実装することができました。