ダイアログ(モーダル)
アクセシビリティとキーボード機能を満載した、完全に管理された、レンダリングレスなダイアログコンポーネントは、次回のアプリケーション用に完全にカスタムなモーダルウィンドウやダイアログウィンドウを構築するのに最適です。
開始するには、npm を介して Headless UI をインストールします。
このライブラリは Vue 3 のみをサポートしていることに注意してください。
npm install @headlessui/vue
ダイアログは、Dialog
、DialogPanel
、DialogTitle
、および DialogDescription
コンポーネントを使用して構築されます。
ダイアログの open
プロパティが true
の場合、ダイアログの内容がレンダリングされます。フォーカスはダイアログ内に移動し、ユーザーがフォーカス可能な要素を循環する際にそこにトラップされます。スクロールがロックされ、アプリケーション UI の残りの部分はスクリーンリーダーから隠され、DialogPanel
の外側をクリックするか、Escape キーを押すと、close
イベントが発生してダイアログが閉じます。
<template> <Dialog :open="isOpen" @close="setIsOpen"> <DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <DialogDescription> This will permanently deactivate your account </DialogDescription> <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 @click="setIsOpen(false)">Deactivate</button> <button @click="setIsOpen(false)">Cancel</button> </DialogPanel> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
ダイアログにタイトルと説明がある場合は、最もアクセシブルなエクスペリエンスを提供するために、DialogTitle
および DialogDescription
コンポーネントを使用してください。これにより、タイトルと説明が、aria-labelledby
および aria-describedby
属性を介してルートダイアログコンポーネントにリンクされ、ダイアログが開いたときに、スクリーンリーダーを使用しているユーザーに内容がアナウンスされるようになります。
ダイアログは、開閉状態の自動管理を行いません。ダイアログを表示および非表示にするには、open
プロパティに ref を渡します。open
が true の場合、ダイアログがレンダリングされ、false の場合はダイアログがアンマウントされます。
close
イベントは、開いているダイアログが閉じられたときに発生します。これは、ユーザーが DialogPanel
の外側をクリックするか、Escape キーを押すと発生します。このイベントを使用して、open
を false に設定してダイアログを閉じることができます。
<template> <!-- Pass the `isOpen` ref to the `open` prop, and use the `close` event to set the ref back to `false` when the user clicks outside of the dialog or presses the escape key. -->
<Dialog :open="isOpen" @close="setIsOpen"><DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <DialogDescription> This will permanently deactivate your account </DialogDescription> <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 your `isOpen` state to `false`. --> <button @click="setIsOpen(false)">Cancel</button> <button @click="handleDeactivate">Deactivate</button></DialogPanel></Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue' // The open/closed state lives outside of the Dialog and // is managed by you. const isOpen = ref(true) function setIsOpen(value) {isOpen.value = value} function handleDeactivate() { // ... } </script>
他の要素と同様に、class
または style
プロパティを使用して Dialog
および DialogPanel
コンポーネントをスタイル設定します。特定のデザインを実現するために、必要に応じて追加の要素を導入することもできます。
<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <div class="fixed inset-0 flex w-screen items-center justify-center p-4"> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { DialogPanel, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
DialogPanel
コンポーネントの外側をクリックするとダイアログが閉じられるため、どの要素に特定のスタイルを適用するかを決定する際は、その点を考慮してください。
DialogPanel
の背後にオーバーレイまたは背景を追加して、パネル自体に注意を引く場合は、背景専用の要素を使用し、それをパネルコンテナの兄弟要素にすることをお勧めします。
<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <!-- The backdrop, rendered as a fixed sibling to the panel container -->
<div class="fixed inset-0 bg-black/30" aria-hidden="true" /><!-- Full-screen container to center the panel --> <div class="fixed inset-0 flex w-screen items-center justify-center p-4"> <!-- The actual dialog panel --> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
これにより、背景とパネルを個別に独自のアニメーションでトランジションできます。また、兄弟としてレンダリングすることで、長いダイアログをスクロールする機能が妨げられることはありません。
ダイアログをスクロール可能にする方法はすべて CSS で処理され、具体的な実装は実現しようとしているデザインによって異なります。
パネルコンテナ全体がスクロール可能で、スクロールするとパネル自体が移動する例を次に示します
<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <!-- The backdrop, rendered as a fixed sibling to the panel container --> <div class="fixed inset-0 bg-black/30" aria-hidden="true" /> <!-- Full-screen scrollable container -->
<div class="fixed inset-0 w-screen overflow-y-auto"><!-- Container to center the panel --><div class="flex min-h-full items-center justify-center p-4"><!-- The actual dialog panel --> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
背景付きのスクロール可能なダイアログを作成する場合は、背景がスクロール可能なコンテナの背後にレンダリングされていることを確認してください。そうしないと、背景の上にマウスを置いたときにスクロールホイールが機能しなくなり、背景がスクロールバーを覆い隠して、ユーザーがマウスでクリックできなくなる可能性があります。
アクセシビリティ上の理由から、ダイアログには少なくとも 1 つのフォーカス可能な要素を含める必要があります。デフォルトでは、Dialog
コンポーネントはレンダリングされると、最初のフォーカス可能な要素(DOM 順)にフォーカスし、Tab キーを押すと、コンテンツ内のすべての追加のフォーカス可能な要素を循環します。
フォーカスは、ダイアログがレンダリングされている限りダイアログ内にトラップされるため、最後にタブを移動すると、最初から再び循環を開始します。ダイアログの外側の他のすべてのアプリケーション要素は、非アクティブとしてマークされるため、フォーカスできなくなります。
ダイアログが最初にレンダリングされたときに、最初のフォーカス可能な要素以外の要素に初期フォーカスを設定する場合は、initialFocus
ref を使用できます。
<template>
<Dialog :initialFocus="completeButtonRef" :open="isOpen" @close="setIsOpen"><DialogPanel> <DialogTitle>Complete your order</DialogTitle> <p>Your order is all ready!</p> <button @click="setIsOpen(false)">Deactivate</button> <!-- Use `initialFocus` to force initial focus to a specific ref. --><button ref="completeButtonRef" @click="completeOrder">Complete order </button> </DialogPanel> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue'const completeButtonRef = ref(null)const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } function completeOrder() { // ... } </script>
以前にダイアログを実装したことがある場合は、おそらくポータルの概念に出会ったことがあるでしょう。ポータルを使用すると、DOM のある場所(たとえば、アプリケーション UI の奥深く)からコンポーネントを呼び出すことができますが、実際には DOM の別の場所に完全にレンダリングできます。
ダイアログとその背景はページ全体を占有するため、通常はそれらをアプリケーションの最上位ノードの兄弟としてレンダリングする必要があります。そうすることで、自然な DOM 順序を利用して、コンテンツが既存のアプリケーション UI の上にレンダリングされるようにすることができます。これにより、アプリケーションの残りの部分にスクロールロックを適用したり、ダイアログのコンテンツと背景がフォーカスとクリックイベントを受け取るのを妨げられないようにしたりすることも簡単になります。
これらのアクセシビリティの問題のため、Headless UI の Dialog
コンポーネントは実際には内部でポータルを使用しています。これにより、妨げられないイベント処理や、アプリケーションの残りの部分を非アクティブにするなどの機能を提供できます。そのため、ダイアログを使用するときは、自分でポータルを使用する必要はありません。すでに処理済みです。
ダイアログの開閉をアニメーション化するには、Headless UI の TransitionRoot
コンポーネントでダイアログをラップし、Dialog
から open
プロパティを削除して、開閉状態を TransitionRoot
の show
プロパティに渡します。
<template> <!-- Wrap your dialog in a `TransitionRoot` to add transitions. -->
<TransitionRoot:show="isOpen"as="template"enter="duration-300 ease-out"enter-from="opacity-0"enter-to="opacity-100"leave="duration-200 ease-in"leave-from="opacity-100"leave-to="opacity-0"><Dialog @close="setIsOpen"> <DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <!-- ... --> <button @click="isOpen = false">Close</button> </DialogPanel> </Dialog></TransitionRoot></template> <script setup> import { ref } from 'vue' import {TransitionRoot,Dialog, DialogPanel, DialogTitle, } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
背景とパネルを別々にアニメーション化するには、TransitionRoot
で Dialog
をラップし、背景とパネルをそれぞれ独自の TransitionChild
でラップします。
<template> <!-- Wrap your dialog in a `TransitionRoot`. -->
<TransitionRoot :show="isOpen" as="template"><Dialog @close="setIsOpen"> <!-- Wrap your backdrop in a `TransitionChild`. --><TransitionChildenter="duration-300 ease-out"enter-from="opacity-0"enter-to="opacity-100"leave="duration-200 ease-in"leave-from="opacity-100"leave-to="opacity-0"><div class="fixed inset-0 bg-black/30" /></TransitionChild><!-- Wrap your panel in a `TransitionChild`. --><TransitionChildenter="duration-300 ease-out"enter-from="opacity-0 scale-95"enter-to="opacity-100 scale-100"leave="duration-200 ease-in"leave-from="opacity-100 scale-100"leave-to="opacity-0 scale-95"><DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <!-- ... --> </DialogPanel></TransitionChild></Dialog> </TransitionRoot> </template> <script setup> import { ref } from 'vue' import {TransitionRoot,TransitionChild,Dialog, DialogPanel, DialogTitle, } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>
Headless UI のトランジションの詳細については、専用のトランジションドキュメントをお読みください。
ダイアログの open
プロパティが true
の場合、ダイアログの内容がレンダリングされ、フォーカスはダイアログ内に移動してそこにトラップされます。DOM 順に従って、最初のフォーカス可能な要素がフォーカスを受け取ります。ただし、initialFocus
ref を使用して、どの要素が最初にフォーカスを受け取るかを制御できます。開いているダイアログで Tab を押すと、すべてのフォーカス可能な要素が循環します。
Dialog
がレンダリングされている場合、DialogPanel
の外側をクリックすると、Dialog
が閉じます。
Dialog
を開くマウス操作は、すぐに使用できる状態では含まれていませんが、通常は <button />
要素を、ダイアログの open
プロパティを true
に切り替える click
ハンドラーに接続します。
コマンド | 説明 |
Esc | 開いているダイアログをすべて閉じます |
Tab | 開いているダイアログの内容を循環します |
Shift + Tab | 開いているダイアログの内容を逆方向に循環します |
ダイアログが開いているとき、スクロールがロックされ、アプリケーション UI の残りの部分はスクリーンリーダーから隠されます。
関連するすべての ARIA 属性が自動的に管理されます。
メインのダイアログコンポーネント。
プロパティ | デフォルト | 説明 |
open | — | ブール値
|
initialFocus | — | HTMLElement 最初にフォーカスを受け取る必要がある要素への ref。 |
as | div | 文字列 | コンポーネント
|
static | false | ブール値 要素が内部で管理された開閉状態を無視するかどうか。 |
unmount | true | ブール値 要素を開閉状態に基づいてアンマウントするか非表示にするか。 |
イベント | 説明 |
close |
|
スロットプロパティ | 説明 |
open |
ダイアログが開いているかどうか。 |
プロパティ | デフォルト | 説明 |
as | div | 文字列 | コンポーネント
|
レンダリングプロパティ | 説明 |
open |
ダイアログが開いているかどうか。 |
プロパティ | デフォルト | 説明 |
as | h2 | 文字列 | コンポーネント
|
スロットプロパティ | 説明 |
open |
ダイアログが開いているかどうか。 |
プロパティ | デフォルト | 説明 |
as | p | 文字列 | コンポーネント
|
スロットプロパティ | 説明 |
open |
ダイアログが開いているかどうか。 |
Headless UI v1.6 以降、DialogOverlay
は非推奨になりました。移行手順については、リリースノートを参照してください。
プロパティ | デフォルト | 説明 |
as | div | 文字列 | コンポーネント
|
スロットプロパティ | 説明 |
open |
ダイアログが開いているかどうか。 |