ダイアログ
アクセシビリティとキーボード機能を備えた、完全に管理された、レンダーレスのダイアログコンポーネントです。完全にカスタムのダイアログやアラートの作成に最適です。
インストール
開始するには、npm を介して Headless UI をインストールします。
npm install @headlessui/react
基本例
ダイアログは、Dialog
、DialogPanel
、DialogTitle
、およびDescription
コンポーネントを使用して構築されます。
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel className="max-w-lg space-y-4 border bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog>
</>
)
}
ダイアログの開閉方法は完全にユーザー次第です。open
プロパティにtrue
を渡すとダイアログが開き、false
を渡すと閉じます。Esc
キーを押したり、DialogPanel
の外側をクリックしたりしてダイアログを閉じるときに、onClose
コールバックも必要です。
スタイル設定
他の要素と同様に、className
またはstyle
プロパティを使用して、Dialog
およびDialogPanel
コンポーネントのスタイルを設定します。必要に応じて、特定のデザインを実現するために追加の要素を導入することもできます。
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
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"> <DialogPanel className="max-w-lg space-y-4 border bg-white p-12"> <DialogTitle>Deactivate account order</DialogTitle>
{/* ... */}
</DialogPanel>
</div>
</Dialog>
)
}
DialogPanel
コンポーネントの外側をクリックするとダイアログが閉じますので、どの要素にどのスタイルを適用するかを決定する際には考慮してください。
例
ダイアログの表示/非表示
ダイアログは制御されたコンポーネントであるため、open
プロパティとonClose
コールバックを使用して、開いている状態を自分で提供および管理する必要があります。
onClose
コールバックは、ユーザーがEscキーを押したり、DialogPanel
の外側をクリックしたりした場合に発生する、ダイアログが閉じられるときに呼び出されます。このコールバックで、open
状態をfalse
に戻してダイアログを閉じます。
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
// The open/closed state lives outside of the `Dialog` and is managed by you
let [isOpen, setIsOpen] = useState(true)
function async handleDeactivate() {
await fetch('/deactivate-account', { method: 'POST' })
setIsOpen(false) }
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)}> <DialogPanel>
<DialogTitle>Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</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>
</DialogPanel>
</Dialog> )
}
開閉状態に簡単にアクセスできない状況では、Headless UIはクリックされたときに最も近いダイアログの祖先を閉じるCloseButton
コンポーネントを提供します。`as`プロパティを使用して、レンダリングされる要素をカスタマイズできます。
import { CloseButton } from '@headlessui/react'
import { MyDialog } from './my-dialog'
import { MyButton } from './my-button'
function Example() {
return (
<MyDialog>
{/* ... */}
<CloseButton as={MyButton}>Cancel</CloseButton> </MyDialog>
)
}
より詳細な制御が必要な場合は、非同期アクションの実行後など、ダイアログを命令的に閉じるために`useClose`フックを使用することもできます。
import { Dialog, useClose } from '@headlessui/react'
function MySearchForm() {
let close = useClose()
return (
<form
onSubmit={async (event) => {
event.preventDefault()
/* Perform search... */
close() }}
>
<input type="search" />
<button type="submit">Submit</button>
</form>
)
}
function Example() {
return (
<Dialog>
<MySearchForm />
{/* ... */}
</Dialog>
)
}
useClose
フックは、Dialog
内にネストされたコンポーネント内で使用する必要があります。そうでない場合は機能しません。
背景の追加
DialogBackdrop
コンポーネントを使用して、ダイアログパネルの背後に背景を追加します。背景をパネルコンテナの兄弟要素にすることをお勧めします。
import { Description, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<DialogBackdrop className="fixed inset-0 bg-black/30" />
{/* 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 */}
<DialogPanel className="max-w-lg space-y-4 bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog>
</>
)
}
これにより、トランジションで背景とパネルを個別にアニメーション化できます。兄弟要素としてレンダリングすることで、長いダイアログのスクロール機能を妨げません。
スクロール可能なダイアログ
ダイアログをスクロール可能にすることは、完全にCSSで処理され、具体的な実装は達成しようとしているデザインによって異なります。
パネルコンテナ全体がスクロール可能で、スクロールするとパネル自体も移動する例を次に示します。
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<div className="fixed inset-0 w-screen overflow-y-auto p-4"> <div className="flex min-h-full items-center justify-center"> <DialogPanel className="max-w-lg space-y-4 border bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div> </div> </Dialog>
</>
)
}
背景付きのスクロール可能なダイアログを作成する際は、背景がスクロール可能なコンテナの*後ろ*にレンダリングされていることを確認してください。そうでない場合、背景にカーソルを合わせたときにスクロールホイールが機能せず、背景がスクロールバーを隠してユーザーがマウスでクリックできなくなる可能性があります。
初期フォーカスの管理
デフォルトでは、Dialog
コンポーネントは開かれたときにダイアログ要素自体にフォーカスを当て、Tabキーを押すとダイアログ内のフォーカス可能な要素が循環します。
レンダリングされている限り、フォーカスはダイアログ内に閉じ込められるため、最後までタブ移動すると、先頭から再び循環が始まります。ダイアログ以外のアプリケーションの他の要素は不活性としてマークされ、フォーカスできません。
ダイアログが開かれたときにダイアログのルート要素以外の要素にフォーカスを当てたい場合は、Headless UIフォームコントロールにautoFocus
プロパティを追加できます。
import { Checkbox, Dialog, DialogPanel, DialogTitle, Field, Label } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(true)
let [isGift, setIsGift] = useState(false)
function completeOrder() {
// ...
}
return (
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
<DialogPanel>
<DialogTitle>Complete your order</DialogTitle>
<p>Your order is all ready!</p>
<Field>
<Checkbox autoFocus value={isGift} onChange={setIsGift} /> <Label>This order is a gift</Label>
</Field>
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={completeOrder}>Complete order</button>
</DialogPanel>
</Dialog>
)
}
フォーカスしたい要素がHeadless UIフォームコントロールでない場合は、代わりにdata-autofocus
属性を追加できます。
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(true)
function completeOrder() {
// ...
}
return (
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
<DialogPanel>
<DialogTitle>Complete your order</DialogTitle>
<p>Your order is all ready!</p>
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button data-autofocus onClick={completeOrder}> Complete order
</button>
</DialogPanel>
</Dialog>
)
}
ポータルへのレンダリング
アクセシビリティ上の問題から、Dialog
コンポーネントは内部的にポータルでレンダリングされます。
ダイアログとその背景はページ全体を占めるため、通常はReactアプリケーションのルートノードの兄弟要素としてレンダリングする必要があります。これにより、自然なDOMの順序を利用して、そのコンテンツが既存のアプリケーションUIの上にレンダリングされるようにすることができます。
これは次のような形でレンダリングされます。
<body>
<div id="your-app">
<!-- ... -->
</div>
<div id="headlessui-portal-root">
<!-- Rendered `Dialog` -->
</div>
</body>
これにより、アプリケーションの残りの部分へのスクロールロックの適用、およびダイアログの内容と背景がフォーカスとクリックイベントを受け取るための障害がないことを確認することも容易になります。
トランジションの追加
ダイアログの開閉をアニメーション化するには、Dialog
コンポーネントにtransition
プロパティを追加し、CSSを使用してトランジションのさまざまな段階のスタイルを設定します。
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
transition className="fixed inset-0 flex w-screen items-center justify-center bg-black/30 p-4 transition duration-300 ease-out data-[closed]:opacity-0" >
<DialogPanel className="max-w-lg space-y-4 bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</Dialog>
</>
)
}
背景とパネルを個別にアニメーション化するには、DialogBackdrop
とDialogPanel
コンポーネントに直接transition
プロパティを追加します。
import { Description, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<DialogBackdrop
transition className="fixed inset-0 bg-black/30 duration-300 ease-out data-[closed]:opacity-0" />
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel
transition className="max-w-lg space-y-4 bg-white p-12 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0" >
<DialogTitle className="text-lg font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog>
</>
)
}
内部的には、transition
プロパティはTransition
コンポーネントとまったく同じ方法で実装されています。詳細については、トランジションドキュメントを参照してください。
Framer Motionを使用したアニメーション
Headless UIは、Framer MotionやReact SpringなどのReactエコシステムの他のアニメーションライブラリともうまく連携します。これらのライブラリにいくつかの状態を公開するだけで済みます。
たとえば、Framer Motionでダイアログをアニメーション化するには、Dialog
コンポーネントにstatic
プロパティを追加し、open
状態に基づいて条件付きでレンダリングします。
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<AnimatePresence>
{isOpen && ( <Dialog static open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50"> <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/30"
/>
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel
as={motion.div}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="max-w-lg space-y-4 bg-white p-12"
>
<DialogTitle className="text-lg font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog> )} </AnimatePresence>
</>
)
}
open
プロパティはスクロールロックとフォーカストラップの管理に使用されますが、static
が存在する限り、実際の要素はopen
値に関係なく常にレンダリングされるため、外部から自分で制御できます。
キーボード操作
コマンド | 説明 |
Esc | 開いているダイアログを閉じます。 |
Tab | 開いているダイアログの内容を循環します。 |
Shift + Tab | 開いているダイアログの内容を逆順に循環します。 |
プロパティ | デフォルト | 説明 |
open | — | ブール値
|
onClose | — | (false) => void
|
as | div | 文字列 | コンポーネント ダイアログがダイアログとしてレンダリングされる要素またはコンポーネント。 |
autoFocus | false | ブール値 最初にレンダリングされたときにダイアログフォーカスを受け取るかどうか。 |
transition | false | ブール値 要素が |
static | false | ブール値 要素が内部的に管理されている開閉状態を無視するかどうか。 |
unmount | true | ブール値 開閉状態に基づいて要素をアンマウントするか、非表示にするかどうか。 |
role | ダイアログ | 'dialog' | 'alertdialog' ダイアログルート要素に適用する |
データ属性 | レンダリングプロップ | 説明 |
data-open | open |
最初にレンダリングされたときにダイアログが開いています。 |
プロパティ | デフォルト | 説明 |
as | div | 文字列 | コンポーネント ダイアログがダイアログ背景としてレンダリングされる要素またはコンポーネント。 |
transition | false | ブール値 要素が |
データ属性 | レンダリングプロップ | 説明 |
data-open | open |
最初にレンダリングされたときにダイアログが開いています。 |
プロパティ | デフォルト | 説明 |
as | div | 文字列 | コンポーネント ダイアログがダイアログパネルとしてレンダリングされる要素またはコンポーネント。 |
transition | false | ブール値 要素が |
データ属性 | レンダリングプロップ | 説明 |
data-open | open |
最初にレンダリングされたときにダイアログが開いています。 |
プロパティ | デフォルト | 説明 |
as | h2 | 文字列 | コンポーネント ダイアログがダイアログタイトルとしてレンダリングされる要素またはコンポーネント。 |
データ属性 | レンダリングプロップ | 説明 |
data-open | open |
最初にレンダリングされたときにダイアログが開いています。 |
プロパティ | デフォルト | 説明 |
as | button | 文字列 | コンポーネント ダイアログが閉じるボタンとしてレンダリングされる要素またはコンポーネント。 |
事前にデザインされたTailwind CSSモーダルとダイアログコンポーネントの例(Headless UIを使用)にご興味のある方は、Tailwind UIをご覧ください。これは、私たちによって美しくデザインされ、熟練した職人技によって作られたコンポーネントのコレクションです。
これは、このようなオープンソースプロジェクトへの私たちの作業を支援する素晴らしい方法であり、それらを改善し、適切に維持することを可能にします。