对话框 (模态)

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

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

请注意,**此库仅支持 Vue 3**。

npm install @headlessui/vue

对话框使用 DialogDialogPanelDialogTitleDialogDescription 组件构建。

当对话框的 open 属性为 true 时,对话框的内容将呈现。焦点将移动到对话框内部并被捕获,因为用户在可聚焦元素之间循环。滚动被锁定,您应用程序 UI 的其余部分对屏幕阅读器隐藏,单击 DialogPanel 外部或按 Escape 键将触发 close 事件并关闭对话框。

<template> <Dialog :open="isOpen" @close="setIsOpen"> <DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <DialogDescription> This will permanently deactivate your account </DialogDescription> <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 @click="setIsOpen(false)">Deactivate</button> <button @click="setIsOpen(false)">Cancel</button> </DialogPanel> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>

如果您的对话框有标题和描述,请使用 DialogTitleDialogDescription 组件提供最易于访问的体验。这将通过 aria-labelledbyaria-describedby 属性将您的标题和描述链接到根对话框组件,确保在您的对话框打开时,其内容会向使用屏幕阅读器的用户宣布。

对话框没有自动管理其打开/关闭状态。要显示和隐藏您的对话框,请将 ref 传递到 open 属性中。当 open 为 true 时,对话框将呈现,当它为 false 时,对话框将卸载。

当打开的对话框被关闭时,close 事件会触发,这发生在用户单击 DialogPanel 外部或按 Escape 键时。您可以使用此事件将 open 设置回 false 并关闭您的对话框。

<template> <!-- Pass the `isOpen` ref to the `open` prop, and use the `close` event to set the ref back to `false` when the user clicks outside of the dialog or presses the escape key. -->
<Dialog :open="isOpen" @close="setIsOpen">
<DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <DialogDescription> This will permanently deactivate your account </DialogDescription> <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 your `isOpen` state to `false`. --> <button @click="setIsOpen(false)">Cancel</button> <button @click="handleDeactivate">Deactivate</button>
</DialogPanel>
</Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue' // The open/closed state lives outside of the Dialog and // is managed by you. const isOpen = ref(true) function setIsOpen(value) {
isOpen.value = value
} function handleDeactivate() { // ... }
</script>

使用 classstyle 属性像对待任何其他元素一样,对 DialogDialogPanel 组件进行样式化。如果需要,您也可以引入额外的元素来实现特定设计。

<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <div class="fixed inset-0 flex w-screen items-center justify-center p-4"> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { DialogPanel, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>

单击 DialogPanel 组件外部将关闭对话框,所以在决定哪个元素应该接收给定样式时请记住这一点。

如果您想在 DialogPanel 后面添加一个覆盖层或遮罩以引起对面板本身的注意,我们建议您使用一个专门用于遮罩的元素,并使其成为您面板容器的同级元素。

<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <!-- The backdrop, rendered as a fixed sibling to the panel container -->
<div class="fixed inset-0 bg-black/30" aria-hidden="true" />
<!-- Full-screen container to center the panel --> <div class="fixed inset-0 flex w-screen items-center justify-center p-4"> <!-- The actual dialog panel --> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>

这使您可以 过渡 遮罩和面板独立使用自己的动画,并将它们作为同级元素呈现可确保它不会干扰您滚动长对话框的能力。

使对话框可滚动完全由 CSS 处理,具体实现取决于您要实现的设计。

以下是一个示例,其中整个面板容器是可滚动的,面板本身会在您滚动时移动。

<template> <Dialog :open="isOpen" @close="setIsOpen" class="relative z-50"> <!-- The backdrop, rendered as a fixed sibling to the panel container --> <div class="fixed inset-0 bg-black/30" aria-hidden="true" /> <!-- Full-screen scrollable container -->
<div class="fixed inset-0 w-screen overflow-y-auto">
<!-- Container to center the panel -->
<div class="flex min-h-full items-center justify-center p-4">
<!-- The actual dialog panel --> <DialogPanel class="w-full max-w-sm rounded bg-white"> <DialogTitle>Complete your order</DialogTitle> <!-- ... --> </DialogPanel> </div> </div> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogTitle, DialogDescription } from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } </script>

在使用遮罩创建可滚动对话框时,请确保遮罩渲染在可滚动容器后面,否则滚动轮在悬停在遮罩上时将无法工作,遮罩可能会遮挡滚动条并阻止用户用鼠标单击它。

出于辅助功能原因,您的对话框应该至少包含一个可聚焦元素。默认情况下,Dialog 组件将在呈现后聚焦第一个可聚焦元素(按 DOM 顺序),按 Tab 键将循环遍历内容中的所有其他可聚焦元素。

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

如果您希望在您的对话框最初呈现时,除第一个可聚焦元素以外的元素接收初始焦点,您可以使用 initialFocus ref。

<template>
<Dialog :initialFocus="completeButtonRef" :open="isOpen" @close="setIsOpen">
<DialogPanel> <DialogTitle>Complete your order</DialogTitle> <p>Your order is all ready!</p> <button @click="setIsOpen(false)">Deactivate</button> <!-- Use `initialFocus` to force initial focus to a specific ref. -->
<button ref="completeButtonRef" @click="completeOrder">
Complete order </button> </DialogPanel> </Dialog> </template> <script setup> import { ref } from 'vue' import { Dialog, DialogPanel, DialogTitle, DialogDescription, } from '@headlessui/vue'
const completeButtonRef = ref(null)
const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value } function completeOrder() { // ... }
</script>

如果您之前曾经实现过 Dialog,那么您可能已经遇到了门户的概念。门户允许您从 DOM 中的一个位置(例如,在您的应用程序 UI 深处)调用组件,但实际上完全渲染到 DOM 的另一个位置。

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

由于这些辅助功能问题,无头 UI 的 Dialog 组件实际上在幕后使用了门户。这样,我们可以提供诸如无阻碍的事件处理和使您的应用程序的其余部分处于惰性状态之类的功能。因此,在使用我们的 Dialog 时,您无需自己使用门户!我们已经处理了它。

要为对话框的打开/关闭添加动画,请将它包裹在无头 UI 的 TransitionRoot 组件中,并从您的 Dialog 中删除 open 属性,而是将您的打开/关闭状态传递到 TransitionRoot 上的 show 属性中。

<template> <!-- Wrap your dialog in a `TransitionRoot` to add transitions. -->
<TransitionRoot
:show="isOpen"
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<Dialog @close="setIsOpen"> <DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <!-- ... --> <button @click="isOpen = false">Close</button> </DialogPanel> </Dialog>
</TransitionRoot>
</template> <script setup> import { ref } from 'vue' import {
TransitionRoot,
Dialog, DialogPanel, DialogTitle, }
from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value }
</script>

要分别为您的遮罩和面板添加动画,请用 TransitionRoot 包裹您的 Dialog,并用各自的 TransitionChild 包裹您的遮罩和面板。

<template> <!-- Wrap your dialog in a `TransitionRoot`. -->
<TransitionRoot :show="isOpen" as="template">
<Dialog @close="setIsOpen"> <!-- Wrap your backdrop in a `TransitionChild`. -->
<TransitionChild
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/30" />
</TransitionChild>
<!-- Wrap your panel in a `TransitionChild`. -->
<TransitionChild
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel> <DialogTitle>Deactivate account</DialogTitle> <!-- ... --> </DialogPanel>
</TransitionChild>
</Dialog> </TransitionRoot> </template> <script setup> import { ref } from 'vue' import {
TransitionRoot,
TransitionChild,
Dialog, DialogPanel, DialogTitle, }
from '@headlessui/vue' const isOpen = ref(true) function setIsOpen(value) { isOpen.value = value }
</script>

要了解有关无头 UI 中过渡的更多信息,请阅读专门的 过渡文档

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

当渲染 Dialog 时,单击 DialogPanel 外部将关闭 Dialog

默认情况下,没有用于打开 Dialog 的鼠标交互,尽管通常您会将 <button /> 元素连接到一个 click 处理程序,该处理程序将 Dialog 的 open 属性切换为 true

命令描述

Esc

关闭任何打开的对话框

Tab

循环遍历打开的对话框的内容

Shift + Tab

向后循环遍历打开的对话框的内容

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

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

主对话框组件。

属性默认值描述
open
Boolean

Dialog 是否打开。

initialFocus
HTMLElement

对应该首先接收焦点的元素的 ref。

asdiv
String | Component

Dialog 应该渲染为的元素或组件。

staticfalse
Boolean

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

unmounttrue
Boolean

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

事件描述
close

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

插槽属性描述
open

Boolean

对话框是否打开。

这指示您实际对话框的面板。单击此组件外部将发出 Dialog 组件上的 close 事件。

属性默认值描述
asdiv
String | Component

DialogPanel 应该渲染为的元素或组件。

渲染属性描述
open

Boolean

对话框是否打开。

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

属性默认值描述
ash2
String | Component

DialogTitle 应该渲染为的元素或组件。

插槽属性描述
open

Boolean

对话框是否打开。

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

属性默认值描述
asp
String | Component

DialogDescription 应该渲染为的元素或组件。

插槽属性描述
open

Boolean

对话框是否打开。

从 Headless UI v1.6 版本开始,DialogOverlay 已被弃用,请查看 发行说明 获取迁移说明。

属性默认值描述
asdiv
String | Component

DialogOverlay 应该渲染为的元素或组件。

插槽属性描述
open

Boolean

对话框是否打开。

如果您对使用 Headless UI 和 Tailwind CSS 的预先设计组件示例感兴趣,请查看 Tailwind UI - 由我们精心打造的一系列精美设计和专业制作的组件。

这是支持我们进行此类开源项目工作的好方法,使我们能够改进它们并保持良好的维护状态。