ダイアログ
<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キーでコンポーネントが自動的に閉じます。
インストール
コマンドラインからコンポーネントをインストールします。
$ npm add radix-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 ダイアログのモーダル性。 |
open | boolean ダイアログの制御された開閉状態。 |
イベント | ペイロード |
---|---|
update:open | [value: boolean] ダイアログの開閉状態が変更されたときに呼び出されるイベントハンドラ。 |
スロット (デフォルト) | ペイロード |
---|---|
open | boolean 現在の開閉状態 |
トリガー
ダイアログを開くボタン
プロパティ | デフォルト | 型 |
---|---|---|
as | 'button' | AsTag | コンポーネント このコンポーネントがレンダリングされる要素またはコンポーネント。 |
asChild | boolean デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。 詳細については、コンポジションガイドをお読みください。 |
データ属性 | 値 |
---|---|
[data-state] | "open" | "closed" |
ポータル
使用すると、オーバーレイとコンテンツのパーツを body
にポータルします。
プロパティ | デフォルト | 型 |
---|---|---|
disabled | boolean テレポートを無効にして、コンポーネントをインラインでレンダリングします | |
forceMount | boolean より細かい制御が必要な場合にマウントを強制するために使用します。Vueアニメーションライブラリでアニメーションを制御する場合に便利です。 | |
to | string | HTMLElement Vueネイティブテレポートコンポーネントプロパティ |
オーバーレイ
ダイアログが開いているときにビューの非アクティブ部分を覆うレイヤー。
プロパティ | デフォルト | 型 |
---|---|---|
as | 'div' | AsTag | コンポーネント このコンポーネントがレンダリングされる要素またはコンポーネント。 |
asChild | boolean デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。 詳細については、コンポジションガイドをお読みください。 | |
forceMount | boolean より細かい制御が必要な場合にマウントを強制するために使用します。Vueアニメーションライブラリでアニメーションを制御する場合に便利です。 |
データ属性 | 値 |
---|---|
[data-state] | "open" | "closed" |
コンテンツ
開いているダイアログにレンダリングされるコンテンツを含みます
プロパティ | デフォルト | 型 |
---|---|---|
as | 'div' | AsTag | コンポーネント このコンポーネントがレンダリングされる要素またはコンポーネント。 |
asChild | boolean デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。 詳細については、コンポジションガイドをお読みください。 | |
disableOutsidePointerEvents | boolean
| |
forceMount | boolean より細かい制御が必要な場合にマウントを強制するために使用します。Vueアニメーションライブラリでアニメーションを制御する場合に便利です。 | |
trapFocus | boolean
|
イベント | ペイロード |
---|---|
closeAutoFocus | [event: Event] 閉じる際に自動フォーカスするときに呼び出されるイベントハンドラ。防止できます。 |
escapeKeyDown | [event: KeyboardEvent] Escapeキーが押されたときに呼び出されるイベントハンドラ。防止できます。 |
focusOutside | [event: FocusOutsideEvent] フォーカスが |
interactOutside | [event: PointerDownOutsideEvent | FocusOutsideEvent]
|
openAutoFocus | [event: Event] 開く際に自動フォーカスするときに呼び出されるイベントハンドラ。防止できます。 |
pointerDownOutside | [event: PointerDownOutsideEvent]
|
データ属性 | 値 |
---|---|
[data-state] | "open" | "closed" |
閉じる
ダイアログを閉じるボタン
プロパティ | デフォルト | 型 |
---|---|---|
as | 'button' | AsTag | コンポーネント このコンポーネントがレンダリングされる要素またはコンポーネント。 |
asChild | boolean デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。 詳細については、コンポジションガイドをお読みください。 |
タイトル
ダイアログが開かれたときにアナウンスされるアクセシブルなタイトル。
タイトルを非表示にする場合は、 <VisuallyHidden asChild>
のように視覚的に非表示ユーティリティで囲みます。
プロパティ | デフォルト | 型 |
---|---|---|
as | 'h2' | AsTag | コンポーネント このコンポーネントがレンダリングされる要素またはコンポーネント。 |
asChild | boolean デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。 詳細については、コンポジションガイドをお読みください。 |
説明
ダイアログが開かれたときにアナウンスされる、オプションのアクセシブルな説明。
説明を非表示にする場合は、 <VisuallyHidden asChild>
のように視覚的に非表示ユーティリティで囲みます。説明を完全に削除する場合は、この部分を削除し、 DialogContent
に :aria-describedby="undefined"
を渡します。
プロパティ | デフォルト | 型 |
---|---|---|
as | 'p' | AsTag | コンポーネント このコンポーネントがレンダリングされる要素またはコンポーネント。 |
asChild | boolean デフォルトでレンダリングされる要素を子として渡された要素に変更し、それらのプロパティと動作をマージします。 詳細については、コンポジションガイドをお読みください。 |
例
ネストされたダイアログ
複数のレイヤーのダイアログをネストできます。
<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>
非同期フォーム送信後に閉じる
制御されたプロパティを使用して、非同期操作の完了後にプログラムでダイアログを閉じます。
<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>
スクロール可能なオーバーレイ
オーバーレイ内のコンテンツを移動して、オーバーフローのあるダイアログをレンダリングします。
// 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>
/* 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
に追加できます。
<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();
}
}"
>
カスタムポータルコンテナ
ダイアログがポータルされる要素をカスタマイズします。
<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>
外部のインタラクションで閉じるのを無効にする
たとえば、クリックしてもダイアログが閉じないようにする必要があるグローバルトースターコンポーネントがある場合などです。
<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デザインパターンに準拠しています。
閉じるアイコンボタン
アイコン(またはフォントアイコン)を提供する場合は、スクリーンリーダーユーザーのために正しくラベル付けすることを忘れないでください。
<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を作成します。
オーバーレイと閉じるボタンの抽象化
この例では、DialogOverlay
とDialogClose
の部分を抽象化しています。
使用方法
<script setup>
import { Dialog, DialogContent, DialogTrigger } from './your-dialog'
</script>
<template>
<Dialog>
<DialogTrigger>Dialog trigger</DialogTrigger>
<DialogContent>Dialog Content</DialogContent>
</Dialog>
</template>
実装
// your-dialog.ts
export { default as DialogContent } from 'DialogContent.vue'
export { DialogRoot as Dialog, DialogTrigger } from 'radix-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>