对话框 (模态)
一个完全管理的、无渲染的对话框组件,充满了辅助功能和键盘功能,非常适合构建完全自定义的模态和对话框窗口,用于您的下一个应用程序。
要开始,请通过 npm 安装 Headless UI
npm install @headlessui/react
对话框是使用 Dialog
、Dialog.Panel
、Dialog.Title
和 Dialog.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.Title
和 Dialog.Description
组件提供最易于访问的体验。这将通过 aria-labelledby
和 aria-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> ) }
使用 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 键将循环遍历内容中的所有其他可聚焦元素。
只要对话框被渲染,焦点就会被捕获在对话框内,因此,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. */ <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 中遇到过 门户。门户让您从 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 (
<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> ) }
要分别动画化背景和面板,请将 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.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 Motion 或 React 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 && (<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
属性仍然用于管理滚动锁定和焦点捕获,但只要存在 static
,实际元素将始终被渲染,无论 open
值如何,这允许您在外部手动控制它。
当对话框的 open
属性为 true
时,对话框的内容将被渲染,并且焦点将被移动到对话框内部并被捕获。第一个可聚焦元素(按 DOM 顺序)将获得焦点,尽管您可以使用 initialFocus
ref 来控制哪个元素获得初始焦点。在打开的对话框上按下 Tab 键会循环遍历所有可聚焦元素。
当 Dialog
被渲染时,单击 Dialog.Panel
外部将关闭 Dialog
。
没有开箱即用的鼠标交互来打开 Dialog
,尽管通常您会将 <button />
元素与 onClick
处理程序连接起来,该处理程序将对话框的 open
属性切换为 true
。
命令 | 描述 |
Esc | 关闭任何打开的对话框 |
Tab | 在打开的对话框的内容中循环 |
Shift + Tab | 在打开的对话框的内容中向后循环 |
属性 | 默认值 | 描述 |
open | — | 布尔值
|
onClose | — | (false) => void 在 |
initialFocus | — | React.MutableRefObject 指向应该首先接收焦点的元素的引用。 |
作为 | div | 字符串 | 组件
|
静态 | false | 布尔值 元素是否应该忽略内部管理的打开/关闭状态。 |
卸载 | true | 布尔值 元素是否应该根据打开/关闭状态卸载或隐藏。 |
渲染道具 | 描述 |
open |
对话框是否打开。 |
属性 | 默认值 | 描述 |
作为 | div | 字符串 | 组件
|
渲染道具 | 描述 |
open |
对话框是否打开。 |
属性 | 默认值 | 描述 |
作为 | h2 | 字符串 | 组件
|
渲染道具 | 描述 |
open |
对话框是否打开。 |
属性 | 默认值 | 描述 |
作为 | p | 字符串 | 组件
|
渲染道具 | 描述 |
open |
对话框是否打开。 |
从 Headless UI v1.6 开始,Dialog.Overlay
已弃用,请参阅 发布说明 以获取迁移说明。
属性 | 默认值 | 描述 |
作为 | div | 字符串 | 组件
|
渲染道具 | 描述 |
open |
对话框是否打开。 |