对话框

一个完全管理的、无渲染的对话框组件,充满了可访问性和键盘功能,非常适合构建完全自定义的对话框和警报。

要开始使用,请通过 npm 安装无头 UI

npm install @headlessui/react

对话框是使用DialogDialogPanelDialogTitleDescription组件构建的

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回调。

使用classNamestyle prop 来设置DialogDialogPanel组件的样式,就像您对任何其他元素一样。如果需要,您还可以引入额外的元素来实现特定的设计。

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 直接添加到DialogBackdropDialogPanel组件

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 MotionReact 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
布尔值

Dialog是否打开。

onClose
(false) => void

Dialog被关闭时调用(通过点击DialogPanel之外或按下Esc键)。通常用于通过将open设置为 false 来关闭对话框。

asdiv
字符串 | 组件

对话框应渲染成的元素或组件。dialog应渲染成的元素或组件。

autoFocusfalse
布尔值

对话框是否在首次渲染时接收焦点。dialog应在首次渲染时接收焦点。

transitionfalse
布尔值

元素是否应渲染过渡属性,如data-closed data-enterdata-leave

staticfalse
布尔值

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

unmounttrue
布尔值

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

roledialog
'dialog' | 'alertdialog'

应用于对话框根元素的role

数据属性渲染道具描述
data-openopen

布尔值

对话框是否在首次渲染时接收焦点。dialog是否打开。

对话框面板后面的视觉背景。

Prop默认值描述
asdiv
字符串 | 组件

对话框应渲染成的元素或组件。对话框背景应渲染成的元素或组件。

transitionfalse
布尔值

元素是否应渲染过渡属性,如data-closed data-enterdata-leave

数据属性渲染道具描述
data-openopen

布尔值

对话框是否在首次渲染时接收焦点。dialog是否打开。

对话框的主要内容区域。点击此组件外部将触发Dialog组件的onClose

Prop默认值描述
asdiv
字符串 | 组件

对话框应渲染成的元素或组件。对话框面板应渲染成的元素或组件。

transitionfalse
布尔值

元素是否应渲染过渡属性,如data-closed data-enterdata-leave

数据属性渲染道具描述
data-openopen

布尔值

对话框是否在首次渲染时接收焦点。dialog是否打开。

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

Prop默认值描述
ash2
字符串 | 组件

对话框应渲染成的元素或组件。对话框标题应渲染成的元素或组件。

数据属性渲染道具描述
data-openopen

布尔值

对话框是否在首次渲染时接收焦点。dialog是否打开。

点击此按钮将关闭最近的Dialog祖先。或者,使用useClose钩子以命令式方式关闭对话框。

Prop默认值描述
as按钮
字符串 | 组件

对话框应渲染成的元素或组件。关闭按钮应渲染成的元素或组件。

如果您有兴趣使用 Headless UI 预先设计Tailwind CSS 模态框和对话框组件示例,请查看Tailwind UI——由我们精心设计和制作的精美组件集合。

这是支持我们参与此类开源项目工作的好方法,并使我们能够改进这些项目并保持其良好维护。