ダイアログ(モーダル)

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

開始するには、npm を介して Headless UI をインストールします。

このライブラリは Vue 3 のみをサポートしていることに注意してください。

npm install @headlessui/vue

ダイアログは、DialogDialogPanelDialogTitle、および 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 プロパティを削除して、開閉状態を TransitionRootshow プロパティに渡します。

<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>

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

<template> <!-- Wrap your dialog in a `TransitionRoot`. -->
<TransitionRoot :show="isOpen" as="template">
<Dialog @close="setIsOpen"> <!-- Wrap your backdrop in a `TransitionChild`. -->
<TransitionChild
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"
>
<div class="fixed inset-0 bg-black/30" />
</TransitionChild>
<!-- Wrap your panel in a `TransitionChild`. -->
<TransitionChild
enter="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
ブール値

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

initialFocus
HTMLElement

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

asdiv
文字列 | コンポーネント

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

staticfalse
ブール値

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

unmounttrue
ブール値

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

イベント説明
close

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

スロットプロパティ説明
open

ブール値

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

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

プロパティデフォルト説明
asdiv
文字列 | コンポーネント

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

レンダリングプロパティ説明
open

ブール値

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

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

プロパティデフォルト説明
ash2
文字列 | コンポーネント

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

スロットプロパティ説明
open

ブール値

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

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

プロパティデフォルト説明
asp
文字列 | コンポーネント

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

スロットプロパティ説明
open

ブール値

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

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

プロパティデフォルト説明
asdiv
文字列 | コンポーネント

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

スロットプロパティ説明
open

ブール値

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

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

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