對話方塊 (模態)

一種完全管理、無需呈現的對話方塊元件,具備豐富的無障礙功能和鍵盤功能,非常適合為您的下一個應用程式建置完全自訂的模態式和對話方塊視窗。

首先,透過 npm 安裝無頭 UI

npm install @headlessui/react

對話方塊使用 DialogDialog.PanelDialog.TitleDialog.Description 元件建置。

當對話方塊的 open 特性為 true 時,對話方塊的內容就會呈現。焦點會移至對話方塊內部並鎖定在那裡,因為使用者會在可焦點的元素之間循環。捲軸會鎖住,應用程式 UI 的其他部分會對螢幕閱讀器隱藏,如果按一下 Dialog.Panel 外部或按下 Esc 鍵,就會觸發 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.TitleDialog.Description 元件,以提供最佳的無障礙體驗。這會透過 aria-labelledbyaria-describedby 屬性連結您的標題和敘述至根對話方塊元件,確保在開啟對話方塊時,螢幕閱讀器使用者可以聽到其內容。

對話方塊不會自動管理其開啟/關閉狀態。要顯示和隱藏對話方塊,請將 React 狀態傳入 open 特性。當 open 為 true 時,對話方塊會呈現,當它為 false 時,對話方塊會卸載。

當開啟的對話方塊被關閉時, onClose 回呼會觸發,這是在使用者按一下 Dialog.Panel 外部或按下 Esc 鍵時發生的情況。您可以使用此回呼將 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> ) }

若要建立一個帶有背景的可滾動對話框,請確認背景呈現在可滾動容器的後方,否則,滑鼠指標移到背景上時,滾輪無法作用,而背景可能會遮住滾動條,讓使用者無法使用滑鼠按一下。

基於可及性的考量,您的對話框應包含至少一個可聚焦的元件。預設情況下,`Dialog` 元件會讓第一個可聚焦的元件(以 DOM 順序)在呈現地點之後取得焦點,而按一下 Tab 鍵會輪詢內容中的所有其他可聚焦的元件。

只要對話框呈現中,焦點就會被鎖在對話框內,因此按 Tab 鍵到最後一個,會開始從頭輪詢。對話框以外的所有其他應用程式元件會標記為惰性的,因而無法聚焦。

如果您希望對話框一開始呈現時,讓第一個可聚焦的元件以外的元件取得初始焦點,您可以使用 `initialFocus` 參照

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. */ <Dialog
initialFocus={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 中看過該 入口。入口讓你能夠呼叫一個地方 (例如你的應用程式 UI 中的深層) 的元件,但實際上會顯示在 DOM 中完全不同的位置。

因為對話框和它們的背景會佔滿整個頁面,你通常會想要將它們顯示為 React 應用的最根節點的同層。透過這種方式,你可以依賴 DOM 的自然順序,以確保他們的內容會顯示在應用程式 UI 的上方。它也讓我們可以輕鬆地對應用的其餘部分套用捲動鎖定,以及確保對話框的內容和背景不會受到阻擋而接收焦點和點擊事件。

因為這些無障礙考量因素,無頭 UI 的對話框元件實際上使用了內建入口。這樣我們就能提供像是不受阻礙的事件處理和讓應用的其餘部分成為慣性的功能。因此,在我們對話框時,你不需要自己使用入口!我們已經處理好了。

要為對話框的開啟/關閉動畫化,請使用 轉場元件。你需要做的就是將 對話框 包裝在 <轉場>,對話框會根據 <轉場>顯示 屬性的狀態自動進行轉場。

<轉場> 與你的對話框一起使用時,你可以移除 開啟 屬性,這是因為對話框會自動從 <轉場> 讀取 顯示 狀態。

import { useState, Fragment } from 'react' import { Dialog, Transition } from '@headlessui/react' function MyDialog() { let [isOpen, setIsOpen] = useState(true) return (
<Transition
show={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> ) }

要分別為背景和面板動畫化,請將你的 對話框 包裹於 轉場 之中,並用它們自己的 轉場.子 來包裹你的背景和面板

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.Child
as={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.Child
as={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 動態React Spring) 為你的對話框動畫化,並需要更多控制,你可以使用 靜態 屬性來告訴無頭 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 && (
<Dialog
static
as={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 屬性為 true,對話框的內容將會呈現,而焦點會移動到對話框內並被鎖定在對話框中。根據 DOM 順序,第一個可聚焦的元素將會獲得焦點,不過你可以使用 initialFocus 參考來控制哪個元素應先獲得焦點。在已開啟對話框中按下 Tab 鍵,會循序往復於所有可聚焦的元素。

Dialog 被呈現時,點選 Dialog.Panel 以外的地方,將會關閉 Dialog

開啟 Dialog 時不包含任何滑鼠互動;一般來說,你可以透過 <button /> 元素串接一個 onClick 處理函式,將 Dialog 的 open 屬性切換為 true

指令說明

Esc

關閉所有已開啟的對話框

Tab

循環瀏覽已開啟對話框的內容

Shift + Tab

循環反向瀏覽已開啟對話框的內容

當對話框開啟時,捲軸會被鎖定,螢幕閱讀程式會隱藏應用程式使用者介面的其他部分。

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

主要的對話框元件。

屬性預設值說明
open
布林

Dialog 是否開啟。

onClose
(false) => void

Dialog 被關閉時呼叫(透過點選 Dialog.Panel 以外的地方,或按一下 Escape 鍵)。通常用於透過將 open 設為 false 來關閉對話框。

initialFocus
React.MutableRefObject

參考應先獲得焦點的元素。

asdiv
字串 | 元件

Dialog 應該呈現的元素或元件。

staticfalse
布林

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

unmounttrue
布林

元素是否應根據開啟/關閉狀態卸載或隱藏。

呈現屬性說明
open

布林

對話方塊是否開啟。

指出實際對話方塊的面板。在這個組件外點擊會觸發 Dialog 組件的 onClose

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

Dialog.Panel 應呈現的元素或組件。

呈現屬性說明
open

布林

對話方塊是否開啟。

您對話方塊的標題。使用時,會在對話方塊上設定 aria-labelledby

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

Dialog.Title 應呈現的元素或組件。

呈現屬性說明
open

布林

對話方塊是否開啟。

您對話方塊的說明。使用時,會在對話方塊上設定 aria-describedby

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

Dialog.Description 應呈現的元素或組件。

呈現屬性說明
open

布林

對話方塊是否開啟。

自 Headless UI v1.6,Dialog.Overlay 已不建議使用,請參閱 發行說明 以取得遷移說明。

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

Dialog.Overlay 應呈現的元素或組件。

呈現屬性說明
open

布林

對話方塊是否開啟。

如果您有興趣瞭解使用 Headless UI 和 Tailwind CSS 的預先設計組件範例,請查看 Tailwind UI — 由我們打造的一組設計精美且製作精良的組件。

這是支援我們這些開源專案的絕佳方式,讓我們得以改進這些專案並確保維護良好。