对话框
一个完全管理的、无渲染的对话框组件,充满了可访问性和键盘功能,非常适合构建完全自定义的对话框和警报。
要开始使用,请通过 npm 安装无头 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
prop 来打开对话框,通过传递false
来关闭它。当对话框通过按下Esc
键或点击DialogPanel
之外时被关闭时,还需要一个onClose
回调。
使用className
或style
prop 来设置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
prop 和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> )
}
对于您无法轻松访问打开/关闭状态的情况,无头 UI 提供了一个CloseButton
组件,当点击时,它将关闭最近的对话框祖先。您可以使用as
prop 来自定义渲染的元素
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键将循环遍历对话框内的任何可聚焦元素。
只要对话框被渲染,焦点就会被困在对话框内,因此按下 Tab 键到最后将开始再次从头开始循环。对话框之外的所有其他应用程序元素将被标记为惰性,因此不可聚焦。
如果您希望在对话框打开时除了对话框的根元素之外的任何元素接收焦点,您可以将autoFocus
prop 添加到任何无头 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>
)
}
如果要聚焦的元素不是无头 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
prop 添加到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
prop 直接添加到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
prop 的实现方式与Transition
组件完全相同。请参阅Transition 文档以了解更多信息。
无头 UI 也与 React 生态系统中的其他动画库(如Framer Motion和React Spring)很好地组合。您只需要向这些库公开一些状态。
例如,要使用 Framer Motion 为对话框添加动画,请将static
prop 添加到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
prop 仍然用于管理滚动锁定和焦点捕获,但只要static
存在,实际元素将始终被渲染,无论open
的值如何,这使您能够在外部自己控制它。
命令 | 描述 |
Esc | 关闭所有打开的对话框 |
Tab | 循环遍历打开的对话框的内容 |
Shift + Tab | 向后循环遍历打开的对话框的内容 |
Prop | 默认值 | 描述 |
open | — | 布尔值
|
onClose | — | (false) => void 当 |
as | div | 字符串 | 组件 对话框应渲染成的元素或组件。dialog应渲染成的元素或组件。 |
autoFocus | false | 布尔值 对话框是否在首次渲染时接收焦点。dialog应在首次渲染时接收焦点。 |
transition | false | 布尔值 元素是否应渲染过渡属性,如 |
static | false | 布尔值 元素是否应忽略内部管理的打开/关闭状态。 |
unmount | true | 布尔值 元素是否应根据打开/关闭状态卸载或隐藏。 |
role | dialog | 'dialog' | 'alertdialog' 应用于对话框根元素的 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
对话框是否在首次渲染时接收焦点。dialog是否打开。 |
Prop | 默认值 | 描述 |
as | div | 字符串 | 组件 对话框应渲染成的元素或组件。对话框背景应渲染成的元素或组件。 |
transition | false | 布尔值 元素是否应渲染过渡属性,如 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
对话框是否在首次渲染时接收焦点。dialog是否打开。 |
Prop | 默认值 | 描述 |
as | div | 字符串 | 组件 对话框应渲染成的元素或组件。对话框面板应渲染成的元素或组件。 |
transition | false | 布尔值 元素是否应渲染过渡属性,如 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
对话框是否在首次渲染时接收焦点。dialog是否打开。 |
Prop | 默认值 | 描述 |
as | h2 | 字符串 | 组件 对话框应渲染成的元素或组件。对话框标题应渲染成的元素或组件。 |
数据属性 | 渲染道具 | 描述 |
data-open | open |
对话框是否在首次渲染时接收焦点。dialog是否打开。 |
Prop | 默认值 | 描述 |
as | 按钮 | 字符串 | 组件 对话框应渲染成的元素或组件。关闭按钮应渲染成的元素或组件。 |
如果您有兴趣使用 Headless UI 预先设计Tailwind CSS 模态框和对话框组件示例,请查看Tailwind UI——由我们精心设计和制作的精美组件集合。
这是支持我们参与此类开源项目工作的好方法,并使我们能够改进这些项目并保持其良好维护。