對話方塊 (模態)

一款全方位管理、無呈現對話方塊元件,內建無障礙與鍵盤功能,適用於為您下一個應用程式打造全客製模態和對話方塊視窗。

開始前,請透過 npm 安裝 Headless UI。

請注意,此程式庫僅支援 Vue 3

npm install @headlessui/vue

對話方塊採用 DialogDialogPanelDialogTitleDialogDescription 元件建立。

當對話方塊的 open 道具為 true 時,對話方塊內容將會呈現。焦點會移至對話方塊內部,並當使用者在可聚焦元素中切換時,焦點將被鎖定在對話方塊內。捲軸將會被鎖定,您應用程式介面的其餘部分將會隱藏在螢幕閱讀程式中,而點擊 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>

對話方塊如果有標題和說明,請使用 DialogTitleDialogDescription 元件來提供最佳的無障礙體驗。這會將您的標題和說明透過 aria-labelledbyaria-describedby 屬性連結至對話方塊根元件,確保當您的對話方塊開啟時,螢幕閱讀程式使用者會聽到這些內容。

對話方塊不會自動管理它們的開啟/關閉狀態。如果要顯示和隱藏對話方塊,請將參考傳遞至 open 道具。當 opentrue 時,對話方塊會呈現,而當 openfalse 時,對話方塊會解除掛載。

當開啟的對話方塊關閉時,會觸發 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>

使用 classstyle 屬性設定 DialogDialogPanel 元件的樣式,就像你對其他元件所做的那樣。如果需要達成特定的設計,你也可以引入其他元件。

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

在使用布景建立可捲動對話方塊時,請確保布景呈現在可捲動容器的 後方,否則當滑鼠游標移到布景時,滾輪將無法作用,而且布景可能會遮蔽滾動條,讓使用者無法用滑鼠按一下它。

基於無障礙考量,你的對話方塊應包含至少一個可獲焦元件。預設情況下,Dialog 元件會在呈現後將焦點集中在第一個可獲焦元件(依 DOM 順序),而按下 Tab 鍵會在內容中的所有其他可獲焦元件之間循環。

對話方塊只要渲染,焦點就會侷限在其中,因此以 Tab 按鍵瀏覽到最後會從頭開始循環。對話框以外的所有其他應用程式元素將標記為不作用,因此無法獲得焦點。

如果您希望在對話框首次渲染時讓最初獲得焦點的不是第一個可獲得焦點的項目,可以使用 initialFocus 參照

<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 的一個位置(例如深在應用程式使用者介面上)呼叫元件,但實際上卻渲染在 DOM 的另一個位置。

由於對話框和其背景會佔滿整個頁面,你通常希望將它們作為應用程式根目錄節點的同層節點來渲染。透過這種方式,你可以仰賴 DOM 的自然排序,以確保其內容會渲染在現有的應用程式使用者介面之上。這也有助於輕鬆地將捲軸鎖定套用至應用程式的其他部分,以及確保對話框的內容和背景暢通無阻地接收焦點和點擊事件。

由於這些無障礙考量,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`. -->
<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 參 chiếu來控制哪個元素接收最初的焦點。在一個開啟的對話框中按下 Tab 鍵,會依序切換所有可接收焦點的元素。

當一個 對話框 呈現時,點擊 對話框面板 外部會關閉 對話框

沒有任何預設的滑鼠互動可以開啟 對話框,不過您通常會連線一個 <button /> 元素和一個 click 處理常式,它會切換對話框的 open 屬性為 true

命令說明

Esc

關閉所有開啟的對話框

Tab

切換一個開啟的對話框中的內容

Shift + Tab

向後切換一個開啟的對話框中的內容

當一個對話框開啟時,捲動功能會被鎖定,而且您應用程式介面的其他部分會對螢幕朗讀器隱藏。

所有相關的 ARIA 屬性會自動管理。

對話框

主要的對話框元件。

屬性預設值說明
open
布林值

對話框 是否開啟或關閉。

initialFocus
HTMLElement

一個應該首先接收焦點的元素的參照。

asdiv
字串 | 元件

對話框 應該呈現為的元素或元件。

staticfalse
布林值

元素是否應該忽略內部管理的開啟/關閉狀態。

unmounttrue
布林值

元素是否應該基於開啟/關閉狀態解除裝載或隱藏。

事件說明
close

對話框 被關閉時發出(透過對 對話框面板 的外部點擊或按下 Escape 鍵)。通常用於設定 open 為 false 以關閉對話框。

Slot 道具說明
open

布林值

對話方塊是否開啟。

這會顯示對話方塊的實際面板。按一下這個元件外側會在 Dialog 元件上傳送 close 事件。

屬性預設值說明
asdiv
字串 | 元件

DialogPanel 應呈現為其元素或元件。

Render 道具說明
open

布林值

對話方塊是否開啟。

這是對話方塊的標題。使用這個標題時,將在對話方塊中設定 aria-labelledby

屬性預設值說明
ash2
字串 | 元件

DialogTitle 應呈現為其元素或元件。

Slot 道具說明
open

布林值

對話方塊是否開啟。

這是對話方塊的說明。使用這個說明時,將在對話方塊中設定 aria-describedby

屬性預設值說明
asp
字串 | 元件

DialogDescription 應呈現為其元素或元件。

Slot 道具說明
open

布林值

對話方塊是否開啟。

從 Headless UI v1.6 版起,DialogOverlay 已停用,請參閱 發行說明,了解遷移說明。

屬性預設值說明
asdiv
字串 | 元件

DialogOverlay 應呈現為其元素或元件。

Slot 道具說明
open

布林值

對話方塊是否開啟。

如果您有興趣使用 Headless UI 和 Tailwind CSS 的預先設計元件範例,請查看 Tailwind UI,這是由我們建立的漂亮設計且專業製作的元件合輯。

這是支援我們在這個類型的開放原始碼專案上所做的工作的最好方法,也能讓我們改善這些專案並持續妥善維護。