コンテンツにスキップ

ダイアログ

プライマリウィンドウまたは別のダイアログウィンドウにオーバーレイ表示されるウィンドウで、下のコンテンツを非アクティブにします。
vue
<script setup lang="ts">
import {
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogOverlay,
  DialogPortal,
  DialogRoot,
  DialogTitle,
  DialogTrigger,
} from 'radix-vue'
import { Icon } from '@iconify/vue'
</script>

<template>
  <DialogRoot>
    <DialogTrigger
      class="text-grass11 font-semibold shadow-blackA7 hover:bg-mauve3 inline-flex h-[35px] items-center justify-center rounded-[4px] bg-white px-[15px] leading-none shadow-[0_2px_10px] focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none"
    >
      Edit profile
    </DialogTrigger>
    <DialogPortal>
      <DialogOverlay class="bg-blackA9 data-[state=open]:animate-overlayShow fixed inset-0 z-30" />
      <DialogContent
        class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
      >
        <DialogTitle class="text-mauve12 m-0 text-[17px] font-semibold">
          Edit profile
        </DialogTitle>
        <DialogDescription class="text-mauve11 mt-[10px] mb-5 text-[15px] leading-normal">
          Make changes to your profile here. Click save when you're done.
        </DialogDescription>
        <fieldset class="mb-[15px] flex items-center gap-5">
          <label
            class="text-grass11 w-[90px] text-right text-[15px]"
            for="name"
          > Name </label>
          <input
            id="name"
            class="text-grass11 shadow-green7 focus:shadow-green8 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-[4px] px-[10px] text-[15px] leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
            defaultValue="Pedro Duarte"
          >
        </fieldset>
        <fieldset class="mb-[15px] flex items-center gap-5">
          <label
            class="text-grass11 w-[90px] text-right text-[15px]"
            for="username"
          > Username </label>
          <input
            id="username"
            class="text-grass11 shadow-green7 focus:shadow-green8 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-[4px] px-[10px] text-[15px] leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
            defaultValue="@peduarte"
          >
        </fieldset>
        <div class="mt-[25px] flex justify-end">
          <DialogClose as-child>
            <button
              class="bg-green4 text-green11 hover:bg-green5 focus:shadow-green7 inline-flex h-[35px] items-center justify-center rounded-[4px] px-[15px] font-semibold leading-none focus:shadow-[0_0_0_2px] focus:outline-none"
            >
              Save changes
            </button>
          </DialogClose>
        </div>
        <DialogClose
          class="text-grass11 hover:bg-green4 focus:shadow-green7 absolute top-[10px] right-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
          aria-label="Close"
        >
          <Icon icon="lucide:x" />
        </DialogClose>
      </DialogContent>
    </DialogPortal>
  </DialogRoot>
</template>

機能

  • モーダルモードと非モーダルモードをサポートします。
  • モーダル時はフォーカスが自動的にトラップされます。
  • 制御することも、制御しないこともできます。
  • Title および Description コンポーネントを使用してスクリーンリーダーのアナウンスを管理します。
  • Escキーでコンポーネントが自動的に閉じます。

インストール

コマンドラインからコンポーネントをインストールします。

sh
$ npm add radix-vue

構造

すべてのパーツをインポートして組み立てます。

vue
<script setup>
import {
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogOverlay,
  DialogPortal,
  DialogRoot,
  DialogTitle,
  DialogTrigger,
} from 'radix-vue'
</script>

<template>
  <DialogRoot>
    <DialogTrigger />
    <DialogPortal>
      <DialogOverlay />
      <DialogContent>
        <DialogTitle />
        <DialogDescription />
        <DialogClose />
      </DialogContent>
    </DialogPortal>
  </DialogRoot>
</template>

APIリファレンス

ルート

ダイアログのすべてのパーツを含みます

プロパティデフォルト
defaultOpen
false
boolean

最初にレンダリングされたときのダイアログの開閉状態。開閉状態を制御する必要がない場合に使用します。

modal
true
boolean

ダイアログのモーダル性。 true に設定すると、
外部要素とのインタラクションが無効になり、ダイアログコンテンツのみがスクリーンリーダーに表示されます。

open
boolean

ダイアログの制御された開閉状態。 v-model:open としてバインドできます。

イベントペイロード
update:open
[value: boolean]

ダイアログの開閉状態が変更されたときに呼び出されるイベントハンドラ。

スロット (デフォルト)ペイロード
open
boolean

現在の開閉状態

トリガー

ダイアログを開くボタン

プロパティデフォルト
as
'button'
AsTag | コンポーネント

このコンポーネントがレンダリングされる要素またはコンポーネント。 asChild によって上書きできます

asChild
boolean

デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。

詳細については、コンポジションガイドをお読みください。

データ属性
[data-state]"open" | "closed"

ポータル

使用すると、オーバーレイとコンテンツのパーツを body にポータルします。

プロパティデフォルト
disabled
boolean

テレポートを無効にして、コンポーネントをインラインでレンダリングします

reference

forceMount
boolean

より細かい制御が必要な場合にマウントを強制するために使用します。Vueアニメーションライブラリでアニメーションを制御する場合に便利です。

to
string | HTMLElement

Vueネイティブテレポートコンポーネントプロパティ :to

reference

オーバーレイ

ダイアログが開いているときにビューの非アクティブ部分を覆うレイヤー。

プロパティデフォルト
as
'div'
AsTag | コンポーネント

このコンポーネントがレンダリングされる要素またはコンポーネント。 asChild によって上書きできます

asChild
boolean

デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。

詳細については、コンポジションガイドをお読みください。

forceMount
boolean

より細かい制御が必要な場合にマウントを強制するために使用します。Vueアニメーションライブラリでアニメーションを制御する場合に便利です。

データ属性
[data-state]"open" | "closed"

コンテンツ

開いているダイアログにレンダリングされるコンテンツを含みます

プロパティデフォルト
as
'div'
AsTag | コンポーネント

このコンポーネントがレンダリングされる要素またはコンポーネント。 asChild によって上書きできます

asChild
boolean

デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。

詳細については、コンポジションガイドをお読みください。

disableOutsidePointerEvents
boolean

true の場合、 DismissableLayer の外部の要素では、ホバー/フォーカス/クリックのインタラクションが無効になります。ユーザーは、外部要素とインタラクトするには、2回クリックする必要があります。1回目は DismissableLayer を閉じ、2回目は要素をトリガーします。

forceMount
boolean

より細かい制御が必要な場合にマウントを強制するために使用します。Vueアニメーションライブラリでアニメーションを制御する場合に便利です。

trapFocus
boolean

true の場合、キーボード、ポインター、またはプログラムによるフォーカスによって、フォーカスが Content からエスケープできなくなります。

イベントペイロード
closeAutoFocus
[event: Event]

閉じる際に自動フォーカスするときに呼び出されるイベントハンドラ。防止できます。

escapeKeyDown
[event: KeyboardEvent]

Escapeキーが押されたときに呼び出されるイベントハンドラ。防止できます。

focusOutside
[event: FocusOutsideEvent]

フォーカスが DismissableLayer の外側に移動したときに呼び出されるイベントハンドラ。防止できます。

interactOutside
[event: PointerDownOutsideEvent | FocusOutsideEvent]

DismissableLayer の外部でインタラクションが発生したときに呼び出されるイベントハンドラ。具体的には、 pointerdown イベントが外部で発生した場合、またはフォーカスが外部に移動した場合です。防止できます。

openAutoFocus
[event: Event]

開く際に自動フォーカスするときに呼び出されるイベントハンドラ。防止できます。

pointerDownOutside
[event: PointerDownOutsideEvent]

DismissableLayer の外部で pointerdown イベントが発生したときに呼び出されるイベントハンドラ。防止できます。

データ属性
[data-state]"open" | "closed"

閉じる

ダイアログを閉じるボタン

プロパティデフォルト
as
'button'
AsTag | コンポーネント

このコンポーネントがレンダリングされる要素またはコンポーネント。 asChild によって上書きできます

asChild
boolean

デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。

詳細については、コンポジションガイドをお読みください。

タイトル

ダイアログが開かれたときにアナウンスされるアクセシブルなタイトル。

タイトルを非表示にする場合は、 <VisuallyHidden asChild> のように視覚的に非表示ユーティリティで囲みます。

プロパティデフォルト
as
'h2'
AsTag | コンポーネント

このコンポーネントがレンダリングされる要素またはコンポーネント。 asChild によって上書きできます

asChild
boolean

デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。

詳細については、コンポジションガイドをお読みください。

説明

ダイアログが開かれたときにアナウンスされる、オプションのアクセシブルな説明。

説明を非表示にする場合は、 <VisuallyHidden asChild> のように視覚的に非表示ユーティリティで囲みます。説明を完全に削除する場合は、この部分を削除し、 DialogContent:aria-describedby="undefined" を渡します。

プロパティデフォルト
as
'p'
AsTag | コンポーネント

このコンポーネントがレンダリングされる要素またはコンポーネント。 asChild によって上書きできます

asChild
boolean

デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。

詳細については、コンポジションガイドをお読みください。

ネストされたダイアログ

複数のレイヤーのダイアログをネストできます。

vue
<script setup lang="ts">
import {
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogOverlay,
  DialogPortal,
  DialogRoot,
  DialogTitle,
  DialogTrigger,
} from 'radix-vue'
import { Icon } from '@iconify/vue'
</script>

<template>
  <div>
    <DialogRoot>
      <DialogTrigger
        class="text-grass11 font-semibold shadow-blackA7 hover:bg-mauve3 inline-flex h-[35px] items-center justify-center rounded-[4px] bg-white px-[15px] leading-none shadow-[0_2px_10px] focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none"
      >
        Open Dialog
      </DialogTrigger>
      <DialogPortal>
        <DialogOverlay class="bg-blackA9 data-[state=open]:animate-overlayShow fixed inset-0 z-30" />
        <DialogContent
          class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
        >
          <DialogTitle class="text-mauve12 m-0 text-[17px] font-semibold">
            First Dialog
          </DialogTitle>
          <DialogDescription class="text-mauve11 mt-[10px] mb-5 text-[15px] leading-normal">
            First dialog.
          </DialogDescription>

          <div class="mt-[25px] flex gap-4 justify-end">
            <DialogClose as-child>
              <button
                class="bg-green4 text-green11 hover:bg-green5 focus:shadow-green7 inline-flex h-[35px] items-center justify-center rounded-[4px] px-[15px] font-semibold leading-none focus:shadow-[0_0_0_2px] focus:outline-none"
              >
                Close
              </button>
            </DialogClose>

            <DialogRoot>
              <DialogTrigger
                class="bg-green9 font-semibold shadow-blackA7 hover:bg-green10  inline-flex h-[35px] items-center justify-center rounded-[4px] text-white px-[15px] leading-none shadow-[0_2px_10px] focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none"
              >
                Open second
              </DialogTrigger>

              <DialogPortal>
                <DialogOverlay class="bg-blackA9 data-[state=open]:animate-overlayShow fixed inset-0 z-30" />
                <DialogContent
                  class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
                >
                  <DialogTitle class="text-mauve12 m-0 text-[17px] font-semibold">
                    Second Dialog
                  </DialogTitle>
                  <DialogDescription class="text-mauve11 mt-[10px] mb-5 text-[15px] leading-normal">
                    Second dialog.
                  </DialogDescription>

                  <div class="flex justify-end">
                    <DialogClose as-child>
                      <button
                        class="bg-green4 text-green11 hover:bg-green5 focus:shadow-green7 inline-flex h-[35px] items-center justify-center rounded-[4px] px-[15px] font-semibold leading-none focus:shadow-[0_0_0_2px] focus:outline-none"
                      >
                        Close
                      </button>
                    </DialogClose>
                  </div>
                </DialogContent>
              </DialogPortal>
            </DialogRoot>
          </div>
          <DialogClose
            class="text-grass11 hover:bg-green4 focus:shadow-green7 absolute top-[10px] right-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
            aria-label="Close"
          >
            <Icon icon="lucide:x" />
          </DialogClose>
        </DialogContent>
      </DialogPortal>
    </DialogRoot>
  </div>
</template>

非同期フォーム送信後に閉じる

制御されたプロパティを使用して、非同期操作の完了後にプログラムでダイアログを閉じます。

vue
<script setup>
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTrigger } from 'radix-vue'

const wait = () => new Promise(resolve => setTimeout(resolve, 1000))
const open = ref(false)
</script>

<template>
  <DialogRoot v-model:open="open">
    <DialogTrigger>Open</DialogTrigger>
    <DialogPortal>
      <DialogOverlay />
      <DialogContent>
        <form
          @submit.prevent="
            (event) => {
              wait().then(() => (open = false));
            }
          "
        >
          <!-- some inputs -->
          <button type="submit">
            Submit
          </button>
        </form>
      </DialogContent>
    </DialogPortal>
  </DialogRoot>
</template>

スクロール可能なオーバーレイ

オーバーレイ内のコンテンツを移動して、オーバーフローのあるダイアログをレンダリングします。

vue
// index.vue
<script setup>
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTrigger } from 'radix-vue'
import './styles.css'
</script>

<template>
  <DialogRoot>
    <DialogTrigger />
    <DialogPortal>
      <DialogOverlay class="DialogOverlay">
        <DialogContent class="DialogContent">
          ...
        </DialogContent>
      </DialogOverlay>
    </DialogPortal>
  </DialogRoot>
</template>
css
/* styles.css */
.DialogOverlay {
  background: rgba(0 0 0 / 0.5);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: grid;
  place-items: center;
  overflow-y: auto;
}

.DialogContent {
  min-width: 300px;
  background: white;
  padding: 30px;
  border-radius: 4px;
}

ただし、このアプローチには注意点があり、ユーザーがスクロールバーをクリックして意図せずにダイアログを閉じてしまう可能性があります。現在、この問題を解決する普遍的な解決策はありませんが、スクロールバーをクリックしたときにモーダルが閉じないように、次のスニペットを DialogContent に追加できます。

vue
<DialogContent
  @pointer-down-outside="(event) => {
    const originalEvent = event.detail.originalEvent;
    const target = originalEvent.target as HTMLElement;
    if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
      event.preventDefault();
    }
  }"
>

カスタムポータルコンテナ

ダイアログがポータルされる要素をカスタマイズします。

vue
<script setup>
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTrigger } from 'radix-vue'

const container = ref(null)
</script>

<template>
  <div>
    <DialogRoot>
      <DialogTrigger />
      <DialogPortal to="container">
        <DialogOverlay />
        <DialogContent>...</DialogContent>
      </DialogPortal>
    </DialogRoot>

    <div ref="container" />
  </div>
</template>

外部のインタラクションで閉じるのを無効にする

たとえば、クリックしてもダイアログが閉じないようにする必要があるグローバルトースターコンポーネントがある場合などです。

vue
<script setup lang="ts">
import {
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogOverlay,
  DialogPortal,
  DialogRoot,
  DialogTitle,
  DialogTrigger,
} from 'radix-vue'
import { Icon } from '@iconify/vue'

import { Toaster, toast } from 'vue-sonner'
</script>

<template>
  <div>
    <DialogRoot>
      <DialogTrigger
        class="text-grass11 font-semibold shadow-blackA7 hover:bg-mauve3 inline-flex h-[35px] items-center justify-center rounded-[4px] bg-white px-[15px] leading-none shadow-[0_2px_10px] focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none"
      >
        Open Dialog
      </DialogTrigger>
      <DialogPortal>
        <DialogOverlay class="bg-blackA9 data-[state=open]:animate-overlayShow fixed inset-0 z-30" />
        <DialogContent
          class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
          @interact-outside="event => {
            const target = event.target as HTMLElement;
            if (target?.closest('[data-sonner-toaster]')) return event.preventDefault()
          }"
        >
          <DialogTitle class="text-mauve12 m-0 text-[17px] font-semibold">
            Dialog Title
          </DialogTitle>
          <DialogDescription class="text-mauve11 mt-[10px] mb-5 text-[15px] leading-normal">
            Dialog description
          </DialogDescription>

          <button
            class="bg-green4 text-green11 hover:bg-green5 focus:shadow-green7 inline-flex h-[35px] items-center justify-center rounded-[4px] px-[15px] font-semibold leading-none focus:shadow-[0_0_0_2px] focus:outline-none"
            @click="() => toast('Event has been created', {
              action: {
                label: 'Undo',
                onClick: () => console.log('Undo'),
              },
            })"
          >
            Give me a toast
          </button>

          <DialogClose
            class="text-grass11 hover:bg-green4 focus:shadow-green7 absolute top-[10px] right-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
            aria-label="Close"
          >
            <Icon icon="lucide:x" />
          </DialogClose>
        </DialogContent>
      </DialogPortal>
    </DialogRoot>

    <ClientOnly>
      <Teleport to="html">
        <Toaster />
      </Teleport>
    </ClientOnly>
  </div>
</template>

アクセシビリティ

ダイアログWAI-ARIAデザインパターンに準拠しています。

閉じるアイコンボタン

アイコン(またはフォントアイコン)を提供する場合は、スクリーンリーダーユーザーのために正しくラベル付けすることを忘れないでください。

html
<DialogRoot>
  <DialogTrigger />
  <DialogPortal>
    <DialogOverlay />
    <DialogContent>
      <DialogTitle />
      <DialogDescription />
      <DialogClose aria-label="Close">
        <span aria-hidden>×</span>
      </DialogClose>
    </DialogContent>
  </DialogPortal>
</DialogRoot>

キーボード操作

キー説明
スペース
ダイアログを開閉します。
Enter
ダイアログを開閉します。
Tab
フォーカスを次のフォーカス可能な要素に移動します。
Shift + Tab
フォーカスを前のフォーカス可能な要素に移動します。
Esc
ダイアログを閉じ、フォーカスをDialogTriggerに移動します。

カスタムAPI

基本的な部分を独自のコンポーネントに抽象化することで、独自のAPIを作成します。

オーバーレイと閉じるボタンの抽象化

この例では、DialogOverlayDialogCloseの部分を抽象化しています。

使用方法

vue
<script setup>
import { Dialog, DialogContent, DialogTrigger } from './your-dialog'
</script>

<template>
  <Dialog>
    <DialogTrigger>Dialog trigger</DialogTrigger>
    <DialogContent>Dialog Content</DialogContent>
  </Dialog>
</template>

実装

ts
// your-dialog.ts
export { default as DialogContent } from 'DialogContent.vue'
export { DialogRoot as Dialog, DialogTrigger } from 'radix-vue'
vue
<!-- DialogContent.vue -->
<script setup lang="ts">
import { DialogClose, DialogContent, type DialogContentEmits, type DialogContentProps, DialogOverlay, DialogPortal, useEmitAsProps, } from 'radix-vue'
import { Cross2Icon } from '@radix-icons/vue'

const props = defineProps<DialogContentProps>()
const emits = defineEmits<DialogContentEmits>()

const emitsAsProps = useEmitAsProps(emits)
</script>

<template>
  <DialogPortal>
    <DialogOverlay />
    <DialogContent v-bind="{ ...props, ...emitsAsProps }">
      <slot />

      <DialogClose>
        <Cross2Icon />
        <span class="sr-only">Close</span>
      </DialogClose>
    </DialogContent>
  </DialogPortal>
</template>