ダイアログ(モーダル)
完全に管理された、レンダリングレスなダイアログコンポーネント。アクセシビリティとキーボード機能が満載で、次のアプリケーションで完全にカスタムなモーダルウィンドウとダイアログウィンドウを構築するのに最適です。
まず、npmを使用してHeadless UIをインストールします
npm install @headlessui/react
ダイアログは、Dialog
、Dialog.Panel
、Dialog.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.Title
とDialog.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. */ <DialoginitialFocus={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 (
<Transitionshow={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> ) }
背景とパネルを別々にアニメーション化するには、Dialog
をTransition
でラップし、背景とパネルをそれぞれ独自の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.Childas={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.Childas={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 MotionやReact 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 && (<Dialogstaticas={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
|
onClose | — | (false) => void
|
initialFocus | — | React.MutableRefObject 最初にフォーカスを受け取る要素へのref。 |
as | div | String | Component
|
static | false | Boolean 要素が内部で管理されている開閉状態を無視するかどうか。 |
unmount | true | Boolean 要素を開閉状態に基づいてアンマウントまたは非表示にするかどうか。 |
レンダリングプロップ | 説明 |
open |
ダイアログが開いているかどうか。 |
プロップ | デフォルト | 説明 |
as | div | String | Component
|
レンダリングプロップ | 説明 |
open |
ダイアログが開いているかどうか。 |
プロップ | デフォルト | 説明 |
as | h2 | String | Component
|
レンダリングプロップ | 説明 |
open |
ダイアログが開いているかどうか。 |
プロップ | デフォルト | 説明 |
as | p | String | Component
|
レンダリングプロップ | 説明 |
open |
ダイアログが開いているかどうか。 |
Headless UI v1.6 以降、Dialog.Overlay
は非推奨になりました。移行手順については、リリースノートを参照してください。
プロップ | デフォルト | 説明 |
as | div | String | Component
|
レンダリングプロップ | 説明 |
open |
ダイアログが開いているかどうか。 |