Floating UI Reactで作るUIコンポーネント 第1回 ポップオーバーUI:パネルの作成と開閉機能の実装

単機能のUIライブラリ「Floating UI React」を使って、ReactでポップオーバーUIを作る方法を解説します。今回は、ポップオーバーのパネルの作成と開閉機能の実装について説明します。

発行

著者 小山田 晃浩 フロントエンド・エンジニア
Floating UI Reactで作るUIコンポーネント シリーズの記事一覧

はじめに

React用のUIコンポーネントを作る際、それが複雑な場合はReact AriaRadix UIといったUIライブラリに頼りたくなるものです。

こうしたUIライブラリは、UIの基本動作に加えて、アクセシビリティやキーボード操作、フォーカス管理などの複雑な機能をまとめて提供してくれます。

一方で、たとえばReact AriaやRadix UIは包括的なUIコンポーネントスイートであり、一度UIライブラリを選択すると、プロジェクト全体で最後まで使い倒す覚悟が必要となってしまいます。1つのプロジェクト内で、モーダルだけReact Aria、ツールチップはRadix UIといった混在は、依存関係が複雑になるので好ましくありません。

実際、React Ariaは、マウス操作などのイベント管理が独特(clickの概念が隠蔽されているなど)で、他のUIライブラリと相性が悪くなる場面があります。

加えて、こうしたUIライブラリが提供する「作り込まれたUI」は、プロジェクトの要件に合わない場合にカスタマイズしたくなることもあります。その場合はライブラリの内部実装を探し、理解し、調整する必要が出てきます。exportされていない機能やクロージャーの中に閉じ込められている機能は、そもそも変更が困難で、コードをフォークする覚悟も必要です。

実際に、筆者もReact Aria利用時に高度なコンボボックスが必要になったため、React Aria内部の一部のファイルをそのまま複製・編集してプロジェクト専用のhooksを作ったことがあります。

筆者はいろいろなプロジェクトでのUI開発の経験を通して、UIライブラリ採用の答えの一つとして、包括的なUIスイートのライブラリを使うのではなく、必要な機能だけを選んで使うことができる、単機能なUIコンポーネントライブラリを使うというところに行き着きました。

現在では、HTML自体に、たとえばdialog要素やdetails要素といった、UIを作るための要素が増えてきています。これらの要素は、ブラウザのネイティブな機能ですので、外部のUIライブラリに頼ることなくUIを実装できます。アクセシビリティや操作性の確保、重なり順の解決も自動で付帯します。

そして、HTMLネイティブの要素では足りない部分は、単機能のUIライブラリを使うことで補えばいいわけです。

単機能のUIライブラリは、たとえば次のようなものがあります。

  • Floating UI React:コンボボックスやカスタムセレクトなどの位置制御、重なり、リスト選択に特化したライブラリ
  • React DayPicker:カレンダー日付入力に特化したライブラリ
  • dnd kit:ドラッグ&ドロップ機能と並べ替えに特化したライブラリ

hooksやProviderを中心に設計されているUIライブラリも多く、その場合は見た目をカスタマイズしやすいです。

Floating UI Reactとは

本シリーズでは、単一の機能特化型のUIライブラリであるFloating UI Reactを使って、ReactでのUIコンポーネントの作成例を解説します。

Floating UI ReactはFloating UIのReact用のラッパーライブラリです。その名前のとおり、ツールチップやポップオーバー、セレクト選択などのUIコンポーネントに特化しています。主な機能として以下が挙げられます。

  • ポップオーバーやツールチップなどのUIの位置決めを制御してくれる
  • スタッキングコンテキストの入れ子や、overflow: hiddenによるパネルの切れを防げる(ポータル機能)
  • リスト選択を上下矢印キーで移動できる

Floating UI Reactは、「Floating UI」の機能に加えて、リスト選択などのReact専用hooksを追加で提供しています。これにより、「フローティングなUI」に加えて、セレクトボックスやコンボボックスもカスタム実装できます。

今回から、第一歩として、Floating UI Reactでポップオーバーを作る例を解説します。続いて、セレクトボックス、コンボボックスを解説していきます。

このシリーズで解説するデモのリポジトリは以下になります。

Floating UI Reactの位置付け

Floating UI自体は、以前はPopper.jsと呼ばれていたライブラリで、ポップオーバーやツールチップなどの位置決めに特化したVanilla JSのライブラリでした。Floating UIはその後継ライブラリです。

そして、React専用にhooksやProviderでFloating UIの機能をラップしたライブラリがFloating UI Reactです。

Floating UI ReactでポップオーバーUIを作る

それでは、この記事で作るポップオーバーUIの完成デモを先に確認しておきましょう。

上記の完成デモで、次のような挙動を確認してみてください。

  • 左下にある「Click me!」のボタンを押すと、ポップオーバーでパネルが表示される
  • 画面下端にボタンがある場合は、パネルはボタン上部に表示される。スクロールして、ボタンが画面の上端にある場合は、パネルは下部に表示され、パネルがビューポート外になることはない
  • 左端にボタンがある場合はパネルが右側に切れず、右端にある場合は左側に切れない

つまり、スクロール位置とトリガーボタンの位置に応じて、パネルが切れない位置に配置される作りにできています。

また、ポップオーバーのパネルは、ボタンクリックで閉じる以外にも、Escキーやパネル以外の場所をクリックすることで閉じることができます。

このように、ポップオーバーとしては高機能なものができています。

ネイティブのポップオーバーAPIもある

HTMLにはネイティブのポップオーバー APIが存在しています。ですからFloating UI Reactに頼らずともポップオーバーUIを実装することもできます(ただし、iOS17未満には対応していませんので、2025年の段階では幅広く使ってもいいかを少し気にする必要がありそうです)。

今回はFloating UI Reactの基本的な使い方を解説するためにポップオーバーUIを題材にしていますが、実はポップオーバーUI程度でしたらネイティブのポップオーバーAPIでも、それほど難しくなく実装できます。

しかし、今後紹介する予定のセレクトボックスやコンボボックスといった、高機能な「浮いたUI」を作るなら、ネイティブでの実装は複雑になってしまうでしょう。そのため、Floating UI Reactといったようなライブラリを使うのがおすすめです。

なお、今後、Floating UI Reactといったライブラリが、逆に内部でネイティブなAPIを使うようにはなるのではと筆者は考えています。ですので、Floating UI Reactに使い慣れておけば、将来、ライブラリを通して間接的にネイティブAPIを利用することになるかもしれませんね。

初期状態の確認

Floating UI Reactを使ったポップオーバーUIのデモ用リポジトリを用意しています。まずは、初期状態のコードを確認してみましょう。

Reactの環境はViteを使い、デモ用のリポジトリに用意してあります。

この状態を初期状態として、Floating UI ReactによるポップオーバーUIを作り進めていきます。この状態では、ボタンを押しても何も起きません。

主に手を加えていくのは、srcにあるApp.tsxのコード部分です。

/src/App.tsx

function App() {

  return (
    <>
      Basic React App<br />
      <button type="button">
        Click Me!
      </button>
    </>
  );
}

export default App;

上記の状態から、次のような手順でポップオーバーUIを作り進めていきます。

  1. Floating UI Reactのインストール
  2. 最低限の機能の割り当て
  3. クリックに対する開閉機能の適用
  4. 閉じやすくする機能の追加
  5. ポータル化による重なりと切れ問題の解消
  6. パネル位置の配置調整
  7. アクセシビリティ付与
  8. 単独コンポーネント化

今回は1から4までの内容を解説します。

1. Floating UI Reactのインストール

まずは、@floating-ui/reactをプロジェクトにインストールします。

npm install @floating-ui/react

Floating UIでは、React用には、以下の2種類のパッケージが提供されています。

  • @floating-ui/react
  • @floating-ui/react-dom

UIを作り込む場合は、@floating-ui/react-domではなく、@floating-ui/reactを選択します。

@floating-ui/react@floating-ui/react-domの違い

@floating-ui/reactは、@floating-ui/react-domの機能に加えて、開閉(マウント・アンマウント)の管理や、ポータル機能など、UIを作り込むうえで便利な機能をexportしています。

@floating-ui/react-domは、位置調整のみの機能をexportしています。

2. 最低限の機能を割り当てる

まず、ポップオーバー用のパネルを配置してみましょう。@floating-ui/reactからuseFloatingをimportします。

useFloatingを使うと以下を関連付け、ポップオーバーのパネルをアンカーとなる要素の付近に展開できます。

  • アンカー、つまりポップオーバーの起点となる要素
  • ポップオーバーのパネル

useFloating関数からは、以下のrefsfloatingStylesの2つの値を取り出します。

  • refsとして、アンカー、パネルのそれぞれに適用するためのref
  • floatingStylesとして、パネルに適用する位置調整の用CSS

取り出した値の1つ目のrefsは、「DOM要素の参照(ref)」を管理するためのオブジェクトです。refs.setReferencerefs.setFloatingが格納されています。これらを、以下のそれぞれを要素のrefとして割り当てます。

  • ポップオーバー起動用のボタンにはrefs.setReference
  • パネルにはrefs.setFloating

2つ目のfloatingStylesは、パネルの位置を調整するためのCSSスタイル情報です。パネルに、useFloatingからfloatingStylesとして取り出した位置計算結果のCSSも適用することで、パネルはボタンの位置に寄り添うように自動配置されます。

ただし、floatingStylesの中には重なり順、つまりz-indexは含まれていません。そのため、他のコンテンツの下に重ならないよう、stylez-indexを指定します。値は任意で自身のプロジェクトに合わせて指定しますが、他のUIとの重なりを考慮して、大きめにしておくとよいでしょう。ここではz-index100と指定しました。

これらを、次のようにApp.tsxに適用してみましょう。

/src/App.tsx

import {
  useFloating,
} from '@floating-ui/react';

function App() {

  const { refs, floatingStyles } = useFloating();

  return (
    <>
      Basic React App<br />
      <button
        type="button"
        ref={ refs.setReference }
      >
        Click Me!
      </button>
      <div
        ref={ refs.setFloating }
        style={ { ...floatingStyles, zIndex: 100, background: '#ccc' } }
      >
        Popover
      </div>
    </>
  );
}

export default App;

ここまでのコードを反映したサンプルは次のようになります。ポップオーバーのパネルが、アンカーとなる要素の付近に展開されました。まだ開閉機能はついていません。

3. クリックに対する開閉機能の適用

続いて、ポップオーバーのパネルに開閉機能を適用していきます。

まず、ReactでuseStateを用いて、表示/非表示用のステート(状態)であるisOpenを用意しておきます。そして、open: isOpenonOpenChange: setIsOpenをuseFloating関数の引数として取り込みます。これにより、パネルの開閉状態をuseFloatingと連動させることができます。(下記コード※1)

合わせて、トリガーボタンにonClickイベントを設定し、onClick時にこのステートが切り替わるようにしておきます。(下記コード※2)

また、パネル自体は、isOpentrueのときだけ表示されるようにし、表示状態に応じた出し分けを行います。(下記コード※3)

これらを反映したコードは次のようになります。

/src/App.tsx

import { useState } from 'react';
import {
  useFloating,
} from '@floating-ui/react';

function App() {

  const [ isOpen, setIsOpen ] = useState( false );
  const { refs, floatingStyles } = useFloating( {
    // ※1
    open: isOpen,
    onOpenChange: setIsOpen,
  } );

  return (
    <>
      Basic React App<br />
      <button
        type="button"
        ref={ refs.setReference }
        onClick={ () => setIsOpen( ! isOpen ) } /* ← 追加 ※2 */
      >
        Click Me!
      </button>
      { isOpen && ( /* ← 追加 ※3 */
        <div
          ref={ refs.setFloating }
          style={ { ...floatingStyles, zIndex: 100, background: '#ccc' } }
        >
          Popover
        </div>
      ) }
    </>
  );
}

export default App;

これにより、ボタンをクリックするごとにパネルの表示・非表示が切り替わるようになりました。

4. 閉じやすくする機能の追加

ここまでで、ボタンをクリックするごとにパネルの表示・非表示が切り替わるようにできました。ただ、ポップオーバーUIは一般的に「補助的な情報」を表示するために使われるので、ボタンのクリック以外にも閉じる実装しておくとユーザーが画面内の操作をしやすくなります。

Floating UI Reactは、パネルを閉じやすくする機能もuseDismissというhooksとして提供しています。

useDismissを利用すると、以下の3つを選択して有効にできます。

  • パネルの外側をクリックで閉じる
  • Escキー押下時に閉じる
  • 画面スクロールしたとき、画面外に出たら閉じる

特に「パネルの外側をクリックで閉じる機能」は、画面内に複数のポップオーバーを配置するときに便利です。これにより結果として、任意のポップオーバーを開いたら、その他のポップオーバーは閉じる、といった動作を実現できます。ポップオーバーAのトリガーをクリックするには、ポップオーバーBの外側をクリックすることになるためです。

この実装を行う主な流れは次の通りです。

  1. useFloatingから、追加でcontextオブジェクトを取り出す(下記コード※1)

  2. そのcontextオブジェクトをuseDisminsの引数として渡し、dismiss機能を有効にする(下記コード※2)

  3. 加えてuseInteractionsの引数にuseDisminsの戻り値を適用し、getReferencePropsgetFloatingPropsの2つを取り出す(下記コード※3)

  4. getReferencePropsgetFloatingPropsは、トリガーとパネルのそれぞれにスプレッドして適用する(下記コード※4a・※4b)

/src/App.tsx

import { useState } from 'react';
import {
  useFloating,
  useDismiss, // 追加
  useInteractions, // 追加
} from '@floating-ui/react';

function App() {

  const [ isOpen, setIsOpen ] = useState( false );
                                // ↓追加 ※1
  const { refs, floatingStyles, context } = useFloating( {
    open: isOpen,
    onOpenChange: setIsOpen,
  } );
  const dismiss = useDismiss( context ); // 追加 ※2
  const { getReferenceProps, getFloatingProps } = useInteractions( [
    dismiss, // 追加 ※3
  ] );

  return (
    <>
      Basic React App<br />
      <button
        type="button"
        ref={ refs.setReference }
        onClick={ () => setIsOpen( ! isOpen ) }
        { ...getReferenceProps() } // 追加 ※4a
      >
        Click Me!
      </button>
      { isOpen && (
        <div
          ref={ refs.setFloating }
          style={ { ...floatingStyles, zIndex: 100, background: '#ccc' } }
          { ...getFloatingProps() } // 追加※4b
        >
          Popover
        </div>
      ) }
    </>
  );
}

export default App;

パネルを展開後、パネルの外側をクリックやEscキーを押下すると、パネルが閉じるようになりました。

ここまでのまとめ

ここまでで、Floating UI Reactを使って、ポップオーバーのパネルを作成し、開閉機能を実装しました。

  1. Floating UI Reactのインストール

  2. 最低限の機能の割り当て

  3. クリックに対する開閉機能の適用

  4. 閉じやすくする機能の追加 👈️ここまで実装

  5. ポータル化による重なりと切れ問題の解消

  6. パネル位置の配置調整

  7. アクセシビリティ付与

  8. 単独コンポーネント化

次回は、ポップオーバーのパネルをポータル化して、重なりと切れ問題を解消する方法から解説します。