對話方塊 (模態)
一種完全管理、無需呈現的對話方塊元件,具備豐富的無障礙功能和鍵盤功能,非常適合為您的下一個應用程式建置完全自訂的模態式和對話方塊視窗。
首先,透過 npm 安裝無頭 UI
npm install @headlessui/react
對話方塊使用 Dialog
、Dialog.Panel
、Dialog.Title
和 Dialog.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.Title
和 Dialog.Description
元件,以提供最佳的無障礙體驗。這會透過 aria-labelledby
和 aria-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. */ <DialoginitialFocus={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 (
<Transitionshow={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.Childas={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.Childas={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 && (<Dialogstaticas={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 | 循環反向瀏覽已開啟對話框的內容 |
屬性 | 預設值 | 說明 |
open | — | 布林
|
onClose | — | (false) => void 當 |
initialFocus | — | React.MutableRefObject 參考應先獲得焦點的元素。 |
as | div | 字串 | 元件
|
static | false | 布林 元素是否應略過內部管理的開啟/關閉狀態。 |
unmount | true | 布林 元素是否應根據開啟/關閉狀態卸載或隱藏。 |
呈現屬性 | 說明 |
open |
對話方塊是否開啟。 |
屬性 | 預設值 | 說明 |
as | div | 字串 | 元件
|
呈現屬性 | 說明 |
open |
對話方塊是否開啟。 |
屬性 | 預設值 | 說明 |
as | h2 | 字串 | 元件
|
呈現屬性 | 說明 |
open |
對話方塊是否開啟。 |
屬性 | 預設值 | 說明 |
as | p | 字串 | 元件
|
呈現屬性 | 說明 |
open |
對話方塊是否開啟。 |
自 Headless UI v1.6,Dialog.Overlay
已不建議使用,請參閱 發行說明 以取得遷移說明。
屬性 | 預設值 | 說明 |
as | div | 字串 | 元件
|
呈現屬性 | 說明 |
open |
對話方塊是否開啟。 |