【React】Headless UIのDialogの使い方を丁寧に解説

ReactでDialog(Modal)を実装するUIライブラリといえば、王道はMaterial UIかと思います。

ですが、今注目を集めているUIライブラリといえばtailwindCSSではないでしょうか。

Material UIとtailwindCSSを組み合わせるのはちょっと使いにくいですよね。

tailwindCSSで作成されたUIライブラリでDialogが実装できたらな〜

これにぴったりなUIライブラリがあります!headless UIです。

本記事ではこのDialogの使い方を詳しく説明します。

サンプルコードの理解

基本的には以下リンクのサンプルコードをコピペすれば実装できてしまいます。

Headless UI
Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.

実際のコードは以下です。

import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'

export default function MyModal() {
  let [isOpen, setIsOpen] = useState(true)

  function closeModal() {
    setIsOpen(false)
  }

  function openModal() {
    setIsOpen(true)
  }

  return (
    <>
      <div className="fixed inset-0 flex items-center justify-center">
        <button
          type="button"
          onClick={openModal}
          className="rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
        >
          Open dialog
        </button>
      </div>

      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={closeModal}>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black bg-opacity-25" />
          </Transition.Child>

          <div className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <Dialog.Title
                    as="h3"
                    className="text-lg font-medium leading-6 text-gray-900"
                  >
                    Payment successful
                  </Dialog.Title>
                  <div className="mt-2">
                    <p className="text-sm text-gray-500">
                      Your payment has been successfully submitted. We’ve sent
                      you an email with all of the details of your order.
                    </p>
                  </div>

                  <div className="mt-4">
                    <button
                      type="button"
                      className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      onClick={closeModal}
                    >
                      Got it, thanks!
                    </button>
                  </div>
                </Dialog.Panel>
              </Transition.Child>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  )
}

げっ、Dialogだけでコード量多くない?って思いますよね。。

以下より一つずつ見ていき、headless UIアレルギーを解消していきましょう!

行われている処理

  1. Open dialogボタンがクリックされ、openModal関数が実行される
  2. isOpenのstateがtrueとなる
  3. TransitionコンポーネントのshowがtrueとなるのでDialogが表示される
  4. closeModalが関数が実行されるとDialogが閉じる

処理自体はシンプルですが、Transition内がやけに記述量が多いのでこれらについて詳細を見ていきましょう。

上記のサンプルコードを貼り付けただけのDialogは以下の状態となっています。

(実装したのは現在個人開発中のクラフトビールです。)

カスタムの詳細

Dialogの構成

Dialogは、Dialogと、Dialog.PanelとDialog.Titleで構成されています。各コンポーネントの特徴を以下に記載します。

  • Dialogはtrueまたはfalseのstateを持っており、openがtrueのときにレンダリングされる
  • DialogのonCloseは、Dialog.Panelの外をクリックするとopenをfalseに変更し、Dialogが閉じる
  • Dialog.Panelの外をクリックするとcloseが発火される
  • Dialog.Titleは、titleを表示しているエリアであることを明示的に表示してくれる

Dialog表示時の背景の表示

Dialog表示時に背景を薄暗くするための設定を以下の部分でしています。

<div className="fixed inset-0 bg-black bg-opacity-25" />

これにより、Dialog.Panelの外側を薄暗くしてくれます。

inset-0は上下左右0を一括指定するもので、これにより画面全体を指定できます。

Transition

TransitionでDialogを囲うことで、Animationを付けることができます。

Dialogのstateは、Transitionのshowのstateに依存します。

そのためTransitionのshowがtrueなら、Dialogが表示されることになります。

本来、Dialogは、以下のようにopen propを持っているものですが、Transitionを使う場合はこのopen propは不要となるというわけです。

<Dialog open={isOpen} onClose={() => setIsOpen(false)}>

更に、backdrop(背景)とpanelを分けたい場合は、Transition.Childを使ってそれぞれ別で囲むようにします。

それが、以下の部分です。

backdropのtransition設定
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black bg-opacity-25" />
          </Transition.Child>

panelのtransition設定
              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <Dialog.Title
                    as="h3"
                    className="text-lg font-medium leading-6 text-gray-900"
                  >
                    Payment successful
                  </Dialog.Title>
                  <div className="mt-2">
                    <p className="text-sm text-gray-500">
                      Your payment has been successfully submitted. We’ve sent
                      you an email with all of the details of your order.
                    </p>
                  </div>

                  <div className="mt-4">
                    <button
                      type="button"
                      className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      onClick={closeModal}
                    >
                      Got it, thanks!
                    </button>
                  </div>
                </Dialog.Panel>
              </Transition.Child>

transitionに設定されている複数のパラメータについて詳しく見てみましょう。

こちらのTransition componentに詳細が説明されています。

以下の部分を一つずつ見ていきます!

          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
as={Fragment}

Transition componentはデフォルトではdiv要素を呼び出します。

as propで、この要素を指定することができます。

この例では、divをFragmentに変更しています。

enter=”ease-out duration-300″

表示されるまでの時間を指定します。

ease-outは加速の種類であり、durationはその動作時間を意味しています。

enterFrom=”opacity-0″

表示の開始状態を指定します。

opacity-0は完全に透明な状態です。そのため、何も表示されていません。

enterTo=”opacity-100″

表示の完了状態を指定します。

opacity-100は完全に表示されている状態です。

leave=”ease-in duration-200″

leaveする(つまり非表示される)ときの動作時間を指定します。

加速方法はease-inで、200msecで非表示となります。

leaveFrom=”opacity-100″

非表示の開始状態を指定します。

leaveTo=”opacity-0″

非表示の完了状態を指定します。

以上がTransitionの詳しい説明になります。

それでは最後にDialogのenter, leaveの仕方を少しいじって終わりにしようと思います。

Dialogが下から出現するようにする

現状、ボタンクリックでpanelは画面中央より出現しますが、下から出現するようにします。

そのためには、panelを囲っているTransition.ChildとPanelに変更を加えます。

              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  

どこから出てきてどこに帰っていくかを変更したいですね。

以下のようにします。

              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 translate-y-full"
                enterTo="opacity-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100"
                leaveTo="opacity-0 translate-y-full"
              >
                <Dialog.Panel className="w-full max-w-md fixed bottom-0 rounded-lg bg-white p-6 text-left shadow-xl">

ポイントは以下になります。

  • translate-y-fullを使用してPanelの高さ分を指定します。
    enterFromでは高さ分隠れており、leaveToでは高さ分隠れることになります。
  • Panelの方はtransfromでoverflow-hiddenが指定されていましたが、bottom:0で指定したいのでfixedとしてbottomからの位置を固定とします。

これにより以下のような動きとすることができました。

いい感じになりましたね!

以上お読みいただきありがとうございました。

コメント