ダイアログ

アクセシビリティとキーボード機能を備えた、完全に管理された、レンダーレスのダイアログコンポーネントです。完全にカスタムのダイアログやアラートの作成に最適です。

インストール

開始するには、npm を介して Headless UI をインストールします。

npm install @headlessui/react

基本例

ダイアログは、DialogDialogPanelDialogTitle、およびDescriptionコンポーネントを使用して構築されます。

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

ダイアログの開閉方法は完全にユーザー次第です。openプロパティにtrueを渡すとダイアログが開き、falseを渡すと閉じます。Escキーを押したり、DialogPanelの外側をクリックしたりしてダイアログを閉じるときに、onCloseコールバックも必要です。

スタイル設定

他の要素と同様に、classNameまたはstyleプロパティを使用して、DialogおよびDialogPanelコンポーネントのスタイルを設定します。必要に応じて、特定のデザインを実現するために追加の要素を導入することもできます。

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キーを押すとダイアログ内のフォーカス可能な要素が循環します。

レンダリングされている限り、フォーカスはダイアログ内に閉じ込められるため、最後までタブ移動すると、先頭から再び循環が始まります。ダイアログ以外のアプリケーションの他の要素は不活性としてマークされ、フォーカスできません。

ダイアログが開かれたときにダイアログのルート要素以外の要素にフォーカスを当てたい場合は、Headless UIフォームコントロールにautoFocusプロパティを追加できます。

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>

これにより、アプリケーションの残りの部分へのスクロールロックの適用、およびダイアログの内容と背景がフォーカスとクリックイベントを受け取るための障害がないことを確認することも容易になります。

トランジションの追加

ダイアログの開閉をアニメーション化するには、Dialogコンポーネントにtransitionプロパティを追加し、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> </> ) }

背景とパネルを個別にアニメーション化するには、DialogBackdropDialogPanelコンポーネントに直接transitionプロパティを追加します。

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コンポーネントとまったく同じ方法で実装されています。詳細については、トランジションドキュメントを参照してください。

Framer Motionを使用したアニメーション

Headless UIは、Framer MotionReact SpringなどのReactエコシステムの他のアニメーションライブラリともうまく連携します。これらのライブラリにいくつかの状態を公開するだけで済みます。

たとえば、Framer Motionでダイアログをアニメーション化するには、Dialogコンポーネントにstaticプロパティを追加し、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

開いているダイアログの内容を逆順に循環します。

コンポーネントAPI

Dialog

メインのダイアログコンポーネント。

プロパティデフォルト説明
open
ブール値

Dialogが開いているかどうか。

onClose
(false) => void

Dialogが閉じられた時(DialogPanelの外側をクリックするか、Escキーを押した場合)に呼び出されます。通常はopenをfalseに設定してダイアログを閉じます。

asdiv
文字列 | コンポーネント

ダイアログがダイアログとしてレンダリングされる要素またはコンポーネント。

autoFocusfalse
ブール値

最初にレンダリングされたときにダイアログフォーカスを受け取るかどうか。

transitionfalse
ブール値

要素がdata-closed data-enterdata-leaveのような遷移属性をレンダリングするかどうか。

staticfalse
ブール値

要素が内部的に管理されている開閉状態を無視するかどうか。

unmounttrue
ブール値

開閉状態に基づいて要素をアンマウントするか、非表示にするかどうか。

roleダイアログ
'dialog' | 'alertdialog'

ダイアログルート要素に適用するrole

データ属性レンダリングプロップ説明
data-openopen

ブール値

最初にレンダリングされたときにダイアログが開いています。

ダイアログパネルの背後にある視覚的な背景。

プロパティデフォルト説明
asdiv
文字列 | コンポーネント

ダイアログがダイアログ背景としてレンダリングされる要素またはコンポーネント。

transitionfalse
ブール値

要素がdata-closed data-enterdata-leaveのような遷移属性をレンダリングするかどうか。

データ属性レンダリングプロップ説明
data-openopen

ブール値

最初にレンダリングされたときにダイアログが開いています。

ダイアログのメインコンテンツ領域。このコンポーネントの外側をクリックすると、DialogコンポーネントのonCloseがトリガーされます。

プロパティデフォルト説明
asdiv
文字列 | コンポーネント

ダイアログがダイアログパネルとしてレンダリングされる要素またはコンポーネント。

transitionfalse
ブール値

要素がdata-closed data-enterdata-leaveのような遷移属性をレンダリングするかどうか。

データ属性レンダリングプロップ説明
data-openopen

ブール値

最初にレンダリングされたときにダイアログが開いています。

これはダイアログのタイトルです。これを使用すると、ダイアログにaria-labelledbyが設定されます。

プロパティデフォルト説明
ash2
文字列 | コンポーネント

ダイアログがダイアログタイトルとしてレンダリングされる要素またはコンポーネント。

データ属性レンダリングプロップ説明
data-openopen

ブール値

最初にレンダリングされたときにダイアログが開いています。

このボタンをクリックすると、最も近いDialog祖先が閉じられます。または、useCloseフックを使用して、ダイアログを強制的に閉じることができます。

プロパティデフォルト説明
asbutton
文字列 | コンポーネント

ダイアログが閉じるボタンとしてレンダリングされる要素またはコンポーネント。

事前にデザインされたTailwind CSSモーダルとダイアログコンポーネントの例(Headless UIを使用)にご興味のある方はTailwind UIをご覧ください。これは、私たちによって美しくデザインされ、熟練した職人技によって作られたコンポーネントのコレクションです。

これは、このようなオープンソースプロジェクトへの私たちの作業を支援する素晴らしい方法であり、それらを改善し、適切に維持することを可能にします。