对话框 (模态)

一个完全管理的、无渲染的对话框组件,充满了辅助功能和键盘功能,非常适合构建完全自定义的模态和对话框窗口,用于您的下一个应用程序。

要开始,请通过 npm 安装 Headless UI

npm install @headlessui/react

对话框是使用 DialogDialog.PanelDialog.TitleDialog.Description 组件构建的。

当对话框的 open 属性为 true 时,对话框的内容将被渲染。焦点将被移动到对话框内并被捕获,因为用户在可聚焦元素之间循环。滚动被锁定,您应用程序 UI 的其余部分对屏幕阅读器隐藏,单击 Dialog.Panel 外部或按下 Escape 键将触发 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 外部或按下 Escape 键时。您可以使用此回调将 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> ) }

使用 classNamestyle 属性像对待任何其他元素一样样式化 DialogDialog.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 键将循环遍历内容中的所有其他可聚焦元素。

只要对话框被渲染,焦点就会被捕获在对话框内,因此,tabbing 到末尾将开始从开头再次循环。对话框之外的所有其他应用程序元素将被标记为惰性,因此不可聚焦。

如果您希望除了第一个可聚焦元素之外的其他元素在对话框最初被渲染时获得初始焦点,您可以使用 initialFocus ref

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 中遇到过 门户。门户让您从 DOM 中的一个位置(例如,在应用程序 UI 中深处)调用组件,但实际上渲染到 DOM 中的另一个位置。

由于对话框及其背景占用了整个页面,因此您通常希望将其渲染为 React 应用程序的根节点的兄弟元素。这样,您可以依靠自然的 DOM 顺序来确保它们的内容渲染在现有应用程序 UI 之上。这也使您可以轻松地将滚动锁定应用于应用程序的其余部分,以及确保对话框的内容和背景不受阻碍地接收焦点和点击事件。

由于这些辅助功能问题,Headless UI 的 Dialog 组件实际上使用了门户。这样,我们可以提供诸如不受阻碍的事件处理以及使应用程序的其余部分变得惰性的功能。因此,当使用我们的对话框时,您无需自己使用门户!我们已经处理好了。

要动画化对话框的打开/关闭,请使用 Transition 组件。您只需将 Dialog 包裹在 <Transition> 中,对话框就会根据 <Transition>show 属性的状态自动进行过渡。

当将 <Transition> 与对话框一起使用时,您可以删除 open 属性,因为对话框将自动从 <Transition> 中读取 show 状态。

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

要分别动画化背景和面板,请将 Dialog 包裹在 Transition 中,并使用各自的 Transition.Child 将背景和面板分别包裹起来。

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 MotionReact Spring)来动画化对话框并需要更多控制,您可以使用 static 属性告诉 Headless 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 属性仍然用于管理滚动锁定和焦点捕获,但只要存在 static,实际元素将始终被渲染,无论 open 值如何,这允许您在外部手动控制它。

当对话框的 open 属性为 true 时,对话框的内容将被渲染,并且焦点将被移动到对话框内部并被捕获。第一个可聚焦元素(按 DOM 顺序)将获得焦点,尽管您可以使用 initialFocus ref 来控制哪个元素获得初始焦点。在打开的对话框上按下 Tab 键会循环遍历所有可聚焦元素。

Dialog 被渲染时,单击 Dialog.Panel 外部将关闭 Dialog

没有开箱即用的鼠标交互来打开 Dialog,尽管通常您会将 <button /> 元素与 onClick 处理程序连接起来,该处理程序将对话框的 open 属性切换为 true

命令描述

Esc

关闭任何打开的对话框

Tab

在打开的对话框的内容中循环

Shift + Tab

在打开的对话框的内容中向后循环

当对话框打开时,滚动被锁定,并且应用程序 UI 的其余部分对屏幕阅读器隐藏。

所有相关的 ARIA 属性都自动管理。

主要的 Dialog 组件。

属性默认值描述
open
布尔值

Dialog 是否打开。

onClose
(false) => void

Dialog 被关闭时调用(通过单击 Dialog.Panel 外部或按下 Escape 键)。通常用于通过将 open 设置为 false 来关闭对话框。

initialFocus
React.MutableRefObject

指向应该首先接收焦点的元素的引用。

作为div
字符串 | 组件

Dialog 应该呈现为的元素或组件。

静态false
布尔值

元素是否应该忽略内部管理的打开/关闭状态。

卸载true
布尔值

元素是否应该根据打开/关闭状态卸载或隐藏。

渲染道具描述
open

布尔值

对话框是否打开。

这表示您实际对话框的面板。单击此组件之外将触发 Dialog 组件的 onClose

属性默认值描述
作为div
字符串 | 组件

Dialog.Panel 应该呈现为的元素或组件。

渲染道具描述
open

布尔值

对话框是否打开。

这是您对话框的标题。使用此选项时,它将设置对话框上的 aria-labelledby

属性默认值描述
作为h2
字符串 | 组件

Dialog.Title 应该呈现为的元素或组件。

渲染道具描述
open

布尔值

对话框是否打开。

这是您对话框的描述。使用此选项时,它将设置对话框上的 aria-describedby

属性默认值描述
作为p
字符串 | 组件

Dialog.Description 应该呈现为的元素或组件。

渲染道具描述
open

布尔值

对话框是否打开。

从 Headless UI v1.6 开始,Dialog.Overlay 已弃用,请参阅 发布说明 以获取迁移说明。

属性默认值描述
作为div
字符串 | 组件

Dialog.Overlay 应该呈现为的元素或组件。

渲染道具描述
open

布尔值

对话框是否打开。

如果您对使用 Headless UI 和 Tailwind CSS 的预设计组件示例感兴趣,请查看 **Tailwind UI** - 我们构建的一组精美设计和精心制作的组件。

这是支持我们对像这样的开源项目工作的绝佳方式,并且使我们能够改进它们并保持良好的维护。