ダイアログ(モーダル)

完全に管理された、レンダリングレスなダイアログコンポーネント。アクセシビリティとキーボード機能が満載で、次のアプリケーションで完全にカスタムなモーダルウィンドウとダイアログウィンドウを構築するのに最適です。

まず、npmを使用してHeadless UIをインストールします

npm install @headlessui/react

ダイアログは、DialogDialog.PanelDialog.Title、およびDialog.Descriptionコンポーネントを使用して構築されます。

ダイアログのopenプロップがtrueの場合、ダイアログの内容がレンダリングされます。フォーカスはダイアログ内に移動し、ユーザーがフォーカス可能な要素を巡回するにつれてそこに固定されます。スクロールはロックされ、アプリケーションUIの残りの部分はスクリーンリーダーから非表示になり、Dialog.Panelの外側をクリックするか、Escapeキーを押すと、closeイベントが発生してダイアログが閉じます。

import { useState } from 'react' import { Dialog } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return ( <Dialog open={isOpen} onClose={() => setIsOpen(false)}> <Dialog.Panel> <Dialog.Title>Deactivate account</Dialog.Title> <Dialog.Description> This will permanently deactivate your account </Dialog.Description> <p> Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone. </p> <button onClick={() => setIsOpen(false)}>Deactivate</button> <button onClick={() => setIsOpen(false)}>Cancel</button> </Dialog.Panel> </Dialog> ) }

ダイアログにタイトルと説明がある場合は、Dialog.TitleDialog.Descriptionコンポーネントを使用して、最もアクセシブルなエクスペリエンスを提供してください。これにより、タイトルと説明がaria-labelledbyおよびaria-describedby属性を介してルートダイアログコンポーネントにリンクされ、ダイアログが開いたときに、スクリーンリーダーを使用しているユーザーにそれらの内容が通知されるようになります。

ダイアログには、開閉状態の自動管理はありません。ダイアログを表示および非表示にするには、Reactの状態をopenプロップに渡します。openがtrueの場合、ダイアログがレンダリングされ、falseの場合はダイアログがアンマウントされます。

onCloseコールバックは、開いているダイアログが閉じられたときに発生します。これは、ユーザーがDialog.Panelの外側をクリックするか、Escapeキーを押したときに発生します。このコールバックを使用して、openをfalseに戻し、ダイアログを閉じることができます。

import { useState } from 'react' import { Dialog } from '@headlessui/react' function MyDialog() { // The open/closed state lives outside of the Dialog and is managed by you
let [isOpen, setIsOpen] = useState(true)
function handleDeactivate() { // ... } return ( /* Pass `isOpen` to the `open` prop, and use `onClose` to set the state back to `false` when the user clicks outside of the dialog or presses the escape key. */
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
<Dialog.Panel> <Dialog.Title>Deactivate account</Dialog.Title> <Dialog.Description> This will permanently deactivate your account </Dialog.Description> <p> Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone. </p> {/* You can render additional buttons to dismiss your dialog by setting `isOpen` to `false`. */} <button onClick={() => setIsOpen(false)}>Cancel</button> <button onClick={handleDeactivate}>Deactivate</button>
</Dialog.Panel>
</Dialog> ) }

他の要素と同様に、classNameまたはstyleプロップを使用して、DialogおよびDialog.Panelコンポーネントをスタイル設定します。特定のデザインを実現するために、必要に応じて追加の要素を導入することもできます。

import { useState } from 'react' import { Dialog } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return ( <Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50" > <div className="fixed inset-0 flex w-screen items-center justify-center p-4"> <Dialog.Panel className="w-full max-w-sm rounded bg-white"> <Dialog.Title>Complete your order</Dialog.Title> {/* ... */} </Dialog.Panel> </div> </Dialog> ) }

Dialog.Panelコンポーネントの外側をクリックするとダイアログが閉じるため、どの要素にどのスタイルを適用するかを決定する際には注意してください。

パネル自体に注意を引くために、Dialog.Panelの背後にオーバーレイまたは背景を追加する場合は、背景専用の要素を使用し、それをパネルコンテナの兄弟にすることをお勧めします。

import { useState } from 'react' import { Dialog } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return ( <Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50" > {/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
{/* Full-screen container to center the panel */} <div className="fixed inset-0 flex w-screen items-center justify-center p-4"> {/* The actual dialog panel */} <Dialog.Panel className="mx-auto max-w-sm rounded bg-white"> <Dialog.Title>Complete your order</Dialog.Title> {/* ... */} </Dialog.Panel> </div> </Dialog> ) }

これにより、背景とパネルをそれぞれのトランジションで別々にアニメーション化でき、兄弟としてレンダリングすることで、長いダイアログをスクロールする機能が妨げられることはありません。

ダイアログをスクロール可能にする処理はすべてCSSで行われ、具体的な実装は実現しようとしているデザインによって異なります。

パネルコンテナ全体がスクロール可能で、スクロールするとパネル自体が移動する例を次に示します。

import { useState } from 'react' import { Dialog } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return ( <Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50" > {/* The backdrop, rendered as a fixed sibling to the panel container */} <div className="fixed inset-0 bg-black/30" aria-hidden="true" /> {/* Full-screen scrollable container */}
<div className="fixed inset-0 w-screen overflow-y-auto">
{/* Container to center the panel */}
<div className="flex min-h-full items-center justify-center p-4">
{/* The actual dialog panel */} <Dialog.Panel className="mx-auto max-w-sm rounded bg-white"> <Dialog.Title>Complete your order</Dialog.Title> {/* ... */} </Dialog.Panel> </div> </div> </Dialog> ) }

背景付きのスクロール可能なダイアログを作成する場合は、背景がスクロール可能なコンテナの後ろにレンダリングされるようにしてください。そうしないと、背景にマウスを合わせてもスクロールホイールが機能せず、背景がスクロールバーを覆い隠して、ユーザーがマウスでクリックできなくなる可能性があります。

アクセシビリティ上の理由から、ダイアログには少なくとも1つのフォーカス可能な要素を含める必要があります。デフォルトでは、Dialogコンポーネントは、レンダリングされると最初のフォーカス可能な要素(DOMの順序で)にフォーカスし、Tabキーを押すと、コンテンツ内の追加のフォーカス可能な要素をすべて巡回します。

フォーカスはダイアログがレンダリングされている間、ダイアログ内に固定されるため、最後にタブで移動すると、再び最初から巡回を開始します。ダイアログの外側の他のすべてのアプリケーション要素は、非アクティブとしてマークされ、フォーカスできなくなります。

ダイアログが最初にレンダリングされたときに、最初のフォーカス可能な要素以外の要素に最初のフォーカスを受け取りたい場合は、initialFocus refを使用できます。

import { useState, useRef } from 'react' import { Dialog } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true)
let completeButtonRef = useRef(null)
function completeOrder() { // ... } return ( /* Use `initialFocus` to force initial focus to a specific ref. */ <Dialog
initialFocus={completeButtonRef}
open={isOpen} onClose={() => setIsOpen(false)} >
<Dialog.Panel> <Dialog.Title>Complete your order</Dialog.Title> <p>Your order is all ready!</p> <button onClick={() => setIsOpen(false)}>Cancel</button>
<button ref={completeButtonRef} onClick={completeOrder}>
Complete order </button> </Dialog.Panel> </Dialog> ) }

以前にダイアログを実装したことがある場合は、Reactのポータルに遭遇したことがあるでしょう。ポータルを使用すると、DOM内の1か所(たとえば、アプリケーションUIの奥深く)からコンポーネントを呼び出すことができますが、実際にはDOM内の別の場所にレンダリングされます。

ダイアログとその背景はページ全体を占めるため、通常はReactアプリケーションの最上位ノードの兄弟としてレンダリングする必要があります。そうすることで、自然なDOMの順序に頼って、既存のアプリケーションUIの上にコンテンツがレンダリングされるようにできます。これにより、アプリケーションの残りの部分にスクロールロックを適用したり、ダイアログのコンテンツと背景がフォーカスとクリックイベントを受け取ることを妨げられないようにしたりすることが容易になります。

これらのアクセシビリティに関する懸念のため、Headless UIのDialogコンポーネントは、実際には内部でポータルを使用しています。このようにして、妨げられないイベント処理や、アプリケーションの残りの部分を非アクティブにするなどの機能を提供できます。したがって、当社のダイアログを使用する場合は、自分でポータルを使用する必要はありません。すでに対応済みです。

ダイアログの開閉をアニメーション化するには、Transitionコンポーネントを使用します。必要なのは、Dialog<Transition>でラップすることだけです。ダイアログは、<Transition>showプロップの状態に基づいて自動的にトランジションします。

ダイアログで<Transition>を使用する場合は、openプロップを削除できます。ダイアログは<Transition>からshow状態を自動的に読み取ります。

import { useState, Fragment } from 'react' import { Dialog, Transition } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return (
<Transition
show={isOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
as={Fragment}
> <Dialog onClose={() => setIsOpen(false)}> <Dialog.Panel> <Dialog.Title>Deactivate account</Dialog.Title> {/* ... */} </Dialog.Panel> </Dialog> </Transition> ) }

背景とパネルを別々にアニメーション化するには、DialogTransitionでラップし、背景とパネルをそれぞれ独自のTransition.Childでラップします。

import { useState, Fragment } from 'react' import { Dialog, Transition } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return ( // Use the `Transition` component at the root level
<Transition show={isOpen} as={Fragment}>
<Dialog onClose={() => setIsOpen(false)}> {/* Use one Transition.Child to apply one transition to the backdrop... */}
<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/30" /> </Transition.Child> {/* ...and another Transition.Child to apply a separate transition to the contents. */}
<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> <Dialog.Title>Deactivate account</Dialog.Title> {/* ... */} </Dialog.Panel> </Transition.Child> </Dialog> </Transition> ) }

Framer MotionReact Springなどの別のアニメーションライブラリを使用してダイアログをアニメーション化し、より多くの制御が必要な場合は、staticプロップを使用して、Headless UIにレンダリング自体を管理しないように指示し、別のツールで手動で制御できます。

import { useState } from 'react' import { Dialog } from '@headlessui/react' import { AnimatePresence, motion } from 'framer-motion' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return ( // Use the `Transition` component + show prop to add transitions.
<AnimatePresence>
{open && (
<Dialog
static
as={motion.div}
open={isOpen}
onClose={() => setIsOpen(false)} > <div className="fixed inset-0 bg-black/30" /> <Dialog.Panel> <Dialog.Title>Deactivate account</Dialog.Title> {/* ... */} </Dialog.Panel> </Dialog> )}
</AnimatePresence>
) }

openプロップは、スクロールロックとフォーカストラップの管理にも使用されますが、staticが存在する限り、実際の要素はopen値に関係なく常にレンダリングされるため、外部から自分で制御できます。

ダイアログのopenプロップがtrueの場合、ダイアログの内容がレンダリングされ、フォーカスがダイアログ内に移動してそこに固定されます。DOMの順序に従って最初のフォーカス可能な要素がフォーカスを受け取りますが、initialFocus refを使用して、どの要素が最初のフォーカスを受け取るかを制御できます。開いているダイアログでTabキーを押すと、すべてのフォーカス可能な要素を巡回します。

Dialogがレンダリングされると、Dialog.Panelの外側をクリックすると、Dialogが閉じます。

Dialogを開くためのマウス操作は、既定では含まれていませんが、通常は、ダイアログのopenプロップをtrueに切り替えるonClickハンドラーで<button />要素をワイヤリングします。

コマンド説明

Esc

開いているダイアログをすべて閉じます

Tab

開いているダイアログの内容を巡回します

Shift + Tab

開いているダイアログの内容を逆方向に巡回します

ダイアログが開いている場合、スクロールはロックされ、アプリケーションUIの残りの部分はスクリーンリーダーから非表示になります。

関連するすべてのARIA属性は自動的に管理されます。

メインのDialogコンポーネント。

プロップデフォルト説明
open
Boolean

Dialogが開いているかどうか。

onClose
(false) => void

Dialogが(Dialog.Panelの外側のクリックまたはEscapeキーを押すことで)閉じられたときに呼び出されます。通常、openをfalseに設定してダイアログを閉じるために使用されます。

initialFocus
React.MutableRefObject

最初にフォーカスを受け取る要素へのref。

asdiv
String | Component

Dialogがレンダリングされる要素またはコンポーネント。

staticfalse
Boolean

要素が内部で管理されている開閉状態を無視するかどうか。

unmounttrue
Boolean

要素を開閉状態に基づいてアンマウントまたは非表示にするかどうか。

レンダリングプロップ説明
open

Boolean

ダイアログが開いているかどうか。

これは実際のダイアログのパネルを示すものです。このコンポーネントの外側をクリックすると、Dialog コンポーネントの onClose がトリガーされます。

プロップデフォルト説明
asdiv
String | Component

Dialog.Panel がレンダリングする要素またはコンポーネント。

レンダリングプロップ説明
open

Boolean

ダイアログが開いているかどうか。

これはダイアログのタイトルです。これを使用すると、ダイアログに aria-labelledby が設定されます。

プロップデフォルト説明
ash2
String | Component

Dialog.Title がレンダリングする要素またはコンポーネント。

レンダリングプロップ説明
open

Boolean

ダイアログが開いているかどうか。

これはダイアログの説明です。これを使用すると、ダイアログに aria-describedby が設定されます。

プロップデフォルト説明
asp
String | Component

Dialog.Description がレンダリングする要素またはコンポーネント。

レンダリングプロップ説明
open

Boolean

ダイアログが開いているかどうか。

Headless UI v1.6 以降、Dialog.Overlay は非推奨になりました。移行手順については、リリースノートを参照してください。

プロップデフォルト説明
asdiv
String | Component

Dialog.Overlay がレンダリングする要素またはコンポーネント。

レンダリングプロップ説明
open

Boolean

ダイアログが開いているかどうか。

Headless UIとTailwind CSSを使用した、デザイン済みのコンポーネント例にご興味がある場合は、当社が作成した美しくデザインされ、専門的に作られたコンポーネントのコレクションである Tailwind UI をご確認ください。

これは、このようなオープンソースプロジェクトでの私たちの活動を支援する素晴らしい方法であり、それらの改善と適切なメンテナンスを可能にします。