對話方塊
具有無障礙性和鍵盤功能的完整管理式無呈現對話方塊元件,非常適合建立完全自訂的對話方塊和警示。
要開始,透過 npm 安裝 Headless UI
npm install @headlessui/react
對話視窗是使用 Dialog
、DialogPanel
、DialogTitle
和 Description
元件建立
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel className="max-w-lg space-y-4 border bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog>
</>
)
}
您使用什麼方式開啟和關閉對話視窗完全取決於您。傳遞 true
至 open
屬性會開啟對話視窗,並傳遞 false
會關閉它。當對話視窗因按 Esc
鍵或在 DialogPanel
外部按一下而關閉時,也需要一個 onClose
回呼。
使用 className
或 style
屬性,套用樣式到 Dialog
及 DialogPanel
元件,就像您對其他元素所做的一樣。您也可以視需要,加入額外的元素以達成特定的設計。
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
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"> <DialogPanel className="max-w-lg space-y-4 border bg-white p-12"> <DialogTitle>Deactivate account order</DialogTitle>
{/* ... */}
</DialogPanel>
</div>
</Dialog>
)
}
按一下 DialogPanel
元件外部會關閉對話視窗,因此在決定要將哪些樣式套用到哪些元素時,請記住這一點。
對話視窗是受控元件,代表您必須自己提供並管理開啟狀態,方法是使用 open
屬性和 onClose
回呼。
當對話視窗被關閉時呼叫 onClose
回呼,這發生在使用者按 Esc 鍵或按一下 DialogPanel
外部時。在此回呼中,將 open
狀態設定回 false
以關閉對話視窗。
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
// The open/closed state lives outside of the `Dialog` and is managed by you
let [isOpen, setIsOpen] = useState(true)
function async handleDeactivate() {
await fetch('/deactivate-account', { method: 'POST' })
setIsOpen(false) }
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)}> <DialogPanel>
<DialogTitle>Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</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>
</DialogPanel>
</Dialog> )
}
在您無法輕鬆存取開啟/關閉狀態的情況下,Headless UI 提供了一個 CloseButton
元件,按一下時它會關閉最近的對話視窗祖先。您可以使用 as
屬性來自訂正在呈現的元素
import { CloseButton } from '@headlessui/react'
import { MyDialog } from './my-dialog'
import { MyButton } from './my-button'
function Example() {
return (
<MyDialog>
{/* ... */}
<CloseButton as={MyButton}>Cancel</CloseButton> </MyDialog>
)
}
如果您需要更多控制,您也可以使用 useClose
掛勾強制作業關閉對話視窗,例如在執行非同步動作後
import { Dialog, useClose } from '@headlessui/react'
function MySearchForm() {
let close = useClose()
return (
<form
onSubmit={async (event) => {
event.preventDefault()
/* Perform search... */
close() }}
>
<input type="search" />
<button type="submit">Submit</button>
</form>
)
}
function Example() {
return (
<Dialog>
<MySearchForm />
{/* ... */}
</Dialog>
)
}
useClose
掛勾必須在嵌套在 Dialog
中的元件中使用,否則它將無法作用。
使用 DialogBackdrop
元件加入對話視窗面板後方的背景幕。建議讓背景幕成為面板容器的兄弟
import { Description, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<DialogBackdrop className="fixed inset-0 bg-black/30" />
{/* 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 */}
<DialogPanel className="max-w-lg space-y-4 bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog>
</>
)
}
這樣你可以變更背景和面板獨立自己的動畫,並將它渲染出來作為兄弟元素確保它不會干擾你捲動長對話框。
讓對話框可捲動完全在 CSS 裡處理,而具體的實作取決於你想達到的設計。
以下是整個面板容器可捲動的範例,而當你捲動時,面板本身也會移動
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<div className="fixed inset-0 w-screen overflow-y-auto p-4"> <div className="flex min-h-full items-center justify-center"> <DialogPanel className="max-w-lg space-y-4 border bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div> </div> </Dialog>
</>
)
}
在建立一個具有背景的可捲動對話框時,請確保背景渲染在可捲動容器後面,否則當指標移到背景時,滑鼠滾輪將無法作用,而且背景可能會遮擋捲軸而讓使用者無法用滑鼠點擊。
預設下,當 Dialog
元件打開時,它會將焦點放在對話框元素本身,然後按下 Tab 鍵會在對話框內的每個可對焦元素間輪替。
只要對話框被渲染出來,焦點就會被困在對話框裡,因此當移動到結尾時,會開始重新從一開始輪替。所有對話框外面的其他應用程式元素會被標記為惰性且無法對焦。
如果你希望在對話框打開時讓其他地方而非對話框的根元素獲得焦點,則可將 autoFocus
屬性新增至任何 Headless UI 表單控制項
import { Checkbox, Dialog, DialogPanel, DialogTitle, Field, Label } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(true)
let [isGift, setIsGift] = useState(false)
function completeOrder() {
// ...
}
return (
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
<DialogPanel>
<DialogTitle>Complete your order</DialogTitle>
<p>Your order is all ready!</p>
<Field>
<Checkbox autoFocus value={isGift} onChange={setIsGift} /> <Label>This order is a gift</Label>
</Field>
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={completeOrder}>Complete order</button>
</DialogPanel>
</Dialog>
)
}
如果你想讓焦點放在的元素不是 Headless UI 表單控制項,則可改為新增 data-autofocus
屬性
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(true)
function completeOrder() {
// ...
}
return (
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
<DialogPanel>
<DialogTitle>Complete your order</DialogTitle>
<p>Your order is all ready!</p>
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button data-autofocus onClick={completeOrder}> Complete order
</button>
</DialogPanel>
</Dialog>
)
}
基於無障礙考量,Dialog
元件會自動在底層的 入口裡渲染。
由於對話框和他們的背景會佔用整個頁面,你通常希望將他們渲染成 React 應用程式最根節點的兄弟元件。如此一來你可以依賴 DOM 的自然順序來確保他們的內容會渲染在現有應用程式 UI 上方。
它的渲染如下所示
<body>
<div id="your-app">
<!-- ... -->
</div>
<div id="headlessui-portal-root">
<!-- Rendered `Dialog` -->
</div>
</body>
這也讓你可以輕鬆地將捲軸鎖定套用至你的其他應用程式,以及確保對話框的內容和背景不會被阻擋,以便獲得焦點及點擊事件。
若要對話框開啟和關閉時的動畫,請將 transition
屬性新增至 Dialog
元件,然後使用 CSS 來設定轉場的不同階段樣式
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
transition className="fixed inset-0 flex w-screen items-center justify-center bg-black/30 p-4 transition duration-300 ease-out data-[closed]:opacity-0" >
<DialogPanel className="max-w-lg space-y-4 bg-white p-12">
<DialogTitle className="font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</Dialog>
</>
)
}
若要個別動畫您的背景與面板,請直接將 transition
屬性新增到 DialogBackdrop
與 DialogPanel
元件中
import { Description, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<DialogBackdrop
transition className="fixed inset-0 bg-black/30 duration-300 ease-out data-[closed]:opacity-0" />
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel
transition className="max-w-lg space-y-4 bg-white p-12 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0" >
<DialogTitle className="text-lg font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog>
</>
)
}
在內部, transition
屬性實作方式與 Transition
元件完全相同。查看 Transition 文件 了解更多資訊。
無頭介面 UI 也能與 React 生態系統中的其他動畫函式庫完美搭配,例如 Framer Motion 和 React Spring。您只需要將一些狀態公開給這些函式庫即可。
例如,若要使用 Framer Motion 動畫化對話方塊,請將 static
屬性新增到 Dialog
元件,再根據 open
狀態有條件地呈現它
import { Description, Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open dialog</button>
<AnimatePresence>
{isOpen && ( <Dialog static open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50"> <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/30"
/>
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel
as={motion.div}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="max-w-lg space-y-4 bg-white p-12"
>
<DialogTitle className="text-lg font-bold">Deactivate account</DialogTitle>
<Description>This will permanently deactivate your account</Description>
<p>Are you sure you want to deactivate your account? All of your data will be permanently removed.</p>
<div className="flex gap-4">
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => setIsOpen(false)}>Deactivate</button>
</div>
</DialogPanel>
</div>
</Dialog> )} </AnimatePresence>
</>
)
}
open
屬性仍用於管理捲動鎖定和焦點鎖定,但只要 static
存在,實際元素始終會呈現,不論 open
值為何,這樣才能讓您自行從外部控制它。
指令 | 描述 |
Esc | 關閉任何開啟的對話方塊 |
Tab | 循環切換已開啟對話方塊的內容 |
Shift + Tab | 往後循環切換已開啟對話方塊的內容 |
屬性 | 預設值 | 描述 |
open | — | 布林值
|
onClose | — | (false) => void
|
as | div | 字串 | 元件 該對話方塊應呈現的元素或元件。 |
autoFocus | false | 布林值 在對話方塊第一次呈現時是否接收焦點。 |
transition | false | 布林值 元素是否應呈現轉場屬性,例如 |
static | false | 布林值 元素是否應忽略內部管理的開啟/關閉狀態。 |
unmount | true | 布林值 根據開啟/關閉狀態,元件應解除安裝還是隱藏。 |
角色 | 對話方塊 | 「對話框」|「快顯對話框」 應用於對話框根元素的 |
資料屬性 | 渲染屬性 | 描述 |
data-open | open |
在對話方塊已開啟。 |
屬性 | 預設值 | 描述 |
as | div | 字串 | 元件 該對話框背景應呈現的元素或元件。 |
transition | false | 布林值 元素是否應呈現轉場屬性,例如 |
資料屬性 | 渲染屬性 | 描述 |
data-open | open |
在對話方塊已開啟。 |
屬性 | 預設值 | 描述 |
as | div | 字串 | 元件 該對話框面板應呈現的元素或元件。 |
transition | false | 布林值 元素是否應呈現轉場屬性,例如 |
資料屬性 | 渲染屬性 | 描述 |
data-open | open |
在對話方塊已開啟。 |
屬性 | 預設值 | 描述 |
as | h2 | 字串 | 元件 該對話框標題應呈現的元素或元件。 |
資料屬性 | 渲染屬性 | 描述 |
data-open | open |
在對話方塊已開啟。 |
屬性 | 預設值 | 描述 |
as | 按鈕 | 字串 | 元件 該關閉按鈕應呈現的元素或元件。 |
如果您有興趣取得預先設計好的 Tailwind CSS 模組及對話框元件範例,請使用 Headless UI,請查看Tailwind UI — 我們所建立的一系列設計精美且製作精良的元件。
這是支援我們從事諸如此類的開源專案的好方法,讓我們能改善這些專案並維持良好維護。