對話方塊

具有無障礙性和鍵盤功能的完整管理式無呈現對話方塊元件,非常適合建立完全自訂的對話方塊和警示。

要開始,透過 npm 安裝 Headless 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>
    </>
  )
}

您使用什麼方式開啟和關閉對話視窗完全取決於您。傳遞 trueopen 屬性會開啟對話視窗,並傳遞 false 會關閉它。當對話視窗因按 Esc 鍵或在 DialogPanel 外部按一下而關閉時,也需要一個 onClose 回呼。

使用 classNamestyle 屬性,套用樣式到 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 屬性和 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>
) }

在您無法輕鬆存取開啟/關閉狀態的情況下,Headless UI 提供了一個 CloseButton 元件,按一下時它會關閉最近的對話視窗祖先。您可以使用 as 屬性來自訂正在呈現的元素

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 鍵會在對話框內的每個可對焦元素間輪替。

只要對話框被渲染出來,焦點就會被困在對話框裡,因此當移動到結尾時,會開始重新從一開始輪替。所有對話框外面的其他應用程式元素會被標記為惰性且無法對焦。

如果你希望在對話框打開時讓其他地方而非對話框的根元素獲得焦點,則可將 autoFocus 屬性新增至任何 Headless 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> ) }

如果你想讓焦點放在的元素不是 Headless 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 屬性新增至 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 屬性新增到 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 屬性實作方式與 Transition 元件完全相同。查看 Transition 文件 了解更多資訊。

無頭介面 UI 也能與 React 生態系統中的其他動畫函式庫完美搭配,例如 Framer MotionReact Spring。您只需要將一些狀態公開給這些函式庫即可。

例如,若要使用 Framer Motion 動畫化對話方塊,請將 static 屬性新增到 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 屬性仍用於管理捲動鎖定和焦點鎖定,但只要 static 存在,實際元素始終會呈現,不論 open 值為何,這樣才能讓您自行從外部控制它。

指令描述

Esc

關閉任何開啟的對話方塊

Tab

循環切換已開啟對話方塊的內容

Shift + Tab

往後循環切換已開啟對話方塊的內容

主要的對話方塊元件。

屬性預設值描述
open
布林值

Dialog 是否開啟。

onClose
(false) => void

Dialog 解除顯示時呼叫(由 DialogPanel 外部按一下或按下 Esc 鍵觸發)。通常用於透過將 open 設定為 false 來關閉對話方塊。

asdiv
字串 | 元件

對話方塊應呈現的元素或元件。

autoFocusfalse
布林值

對話方塊第一次呈現時是否接收焦點。

transitionfalse
布林值

元素是否應呈現轉場屬性,例如 data-closed data-enterdata-leave

staticfalse
布林值

元素是否應忽略內部管理的開啟/關閉狀態。

unmounttrue
布林值

根據開啟/關閉狀態,元件應解除安裝還是隱藏。

角色對話方塊
「對話框」|「快顯對話框」

應用於對話框根元素的角色

資料屬性渲染屬性描述
data-openopen

布林值

對話方塊已開啟。

對話框面板後方的視覺背景。

屬性預設值描述
asdiv
字串 | 元件

對話框背景應呈現的元素或元件。

transitionfalse
布林值

元素是否應呈現轉場屬性,例如 data-closed data-enterdata-leave

資料屬性渲染屬性描述
data-openopen

布林值

對話方塊已開啟。

對話框的主要內容區域。按一下此元件外部將會觸發對話框元件的onClose

屬性預設值描述
asdiv
字串 | 元件

對話框面板應呈現的元素或元件。

transitionfalse
布林值

元素是否應呈現轉場屬性,例如 data-closed data-enterdata-leave

資料屬性渲染屬性描述
data-openopen

布林值

對話方塊已開啟。

這是對話框的標題。使用此標題時,將在對話框上設定aria-labelledby

屬性預設值描述
ash2
字串 | 元件

對話框標題應呈現的元素或元件。

資料屬性渲染屬性描述
data-openopen

布林值

對話方塊已開啟。

按一下此按鈕將會關閉最近的對話框母系。或者,使用useClose 掛鉤強制關閉對話框。

屬性預設值描述
as按鈕
字串 | 元件

關閉按鈕應呈現的元素或元件。

如果您有興趣取得預先設計好的 Tailwind CSS 模組及對話框元件範例,請使用 Headless UI,請查看Tailwind UI — 我們所建立的一系列設計精美且製作精良的元件。

這是支援我們從事諸如此類的開源專案的好方法,讓我們能改善這些專案並維持良好維護。