メッセージを表示したり、ユーザ入力を要求したりするために使用できるダイアログウィンドウ。

使用法

Button、またはモーダルのデフォルトスロットにある他のコンポーネントを使用します。

次に、#contentスロットを使用して、Modalが開いているときに表示されるコンテンツを追加します。

<template>
  <UModal>
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UModal>
</template>

#header、#body、#footerスロットを使用してModalのコンテンツをカスタマイズすることもできます。

Title

titleプロパティを使用して、Modalのヘッダーのタイトルを設定します。

<template>
  <UModal title="Modal with title">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>

Description

descriptionプロパティを使用して、Modalのヘッダーの説明を設定します。

<template>
  <UModal
    title="Modal with description"
    description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
  >
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>

閉じる

closeプロパティを使用して、Modalのヘッダーに表示される閉じるボタン(false値)をカスタマイズまたは非表示にします。

Buttonコンポーネントから任意のプロパティを渡してカスタマイズできます。

<template>
  <UModal
    title="Modal with close button"
    :close="{
      color: 'primary',
      variant: 'outline',
      class: 'rounded-full'
    }"
  >
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>
#contentスロットがヘッダの一部であるため、閉じるボタンは表示されません。

アイコンを閉じる

close-iconプロパティを使用して、閉じるボタンIconをカスタマイズします。デフォルトはi-lucide-xです。

<template>
  <UModal title="Modal with close button" close-icon="i-lucide-arrow-right">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>
このアイコンはapp.config.tsのui.icons.closeキーでグローバルにカスタマイズできます。
このアイコンはvite.config.tsのui.icons.closeキーでグローバルにカスタマイズできます。

Transition

transitionプロパティを使用して、モーダルがアニメーション化されているかどうかを制御します。デフォルトはtrueです。

<template>
  <UModal :transition="false" title="Modal without transition">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>

オーバーレイ

overlayプロパティを使用して、モーダルにオーバーレイがあるかどうかを制御します。デフォルトはtrueです。

<template>
  <UModal :overlay="false" title="Modal without overlay">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>

modalプロパティを使用して、Modalが外部コンテンツとのインタラクションをブロックするかどうかを制御します。デフォルトはtrueです。

modalをfalseに設定すると、オーバーレイは自動的に無効になり、外部コンテンツはインタラクティブになります。
<template>
  <UModal :modal="false" title="Modal interactive">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>

Dismissible

dismissibleプロパティを使用して、Modalの外側をクリックしたりescapeを押したりしたときにDismissibleかどうかを制御します。デフォルトはtrueです。

close:preventイベントは、ユーザーがクローズしようとすると発行されます。
modal: falseとdismissible: falseを組み合わせると、Modalの背景を閉じずにインタラクティブにすることができます。
<template>
  <UModal :dismissible="false" title="Modal non-dismissible">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>

スクロール可能badge{label="4.2+" class="align-text-top"}

scrollableプロパティを使用して、モーダルのコンテンツをオーバーレイ内でスクロールできるようにします。

スクロールにオーバーレイが必要なため、modal: falseは互換性がなく、overlay: falseは背景を削除するだけです。
<template>
  <UModal scrollable title="Modal scrollable">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </UModal>
</template>
オペレーティングシステムによってはスクロールバーをクリックすると意図せずダイアログが閉じてしまうknown issueがあります。

フルスクリーン

fullscreenプロパティを使用してModalをフルスクリーンにします。

<template>
  <UModal fullscreen title="Modal fullscreen">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </UModal>
</template>

アンマウントbadge{label="4.10+" class="align-text-top"}

unmount-on-hideプロパティを使用して、Modalのコンテンツがクローズされたときにアンマウントされないようにします。デフォルトはtrueです。

<template>
  <UModal :unmount-on-hide="false" title="Modal">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UModal>
</template>
DOMを調べると、Modalのコンテンツが閉じている間でもレンダリングされていることがわかります。
portalプロパティがfalseに設定されている場合、コンテンツもサーバー上でレンダリングされます。これはSSR中に開いているModalをページ読み込み時にフラッシュなしでレンダリングしたり、SEOのためにコンテンツを公開したりするのに便利です。

例

Controlオープンステート

オープン状態はdefault-openプロパティまたはv-model:openディレクティブを使用して制御できます。

<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UModal v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UModal>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <UModal v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UModal>
</template>
この例では、defineShortcutsを活用して、kbd{value="O"}を押してモーダルを切り替えることができます。
これにより、トリガーをモーダルの外側に移動したり、完全に削除したりできます。

プログラムの使用法

useOverlayコンポーザブルを使用して、プログラムでModalを開くことができます。

OverlayProviderコンポーネントを使用するAppコンポーネントでアプリをラップしてください。

まず、プログラムで開くモーダルコンポーネントを作成します。

ModalExample.vue
<script setup lang="ts">
defineProps<{
  count: number
}>()

const emit = defineEmits<{ close: [boolean] }>()
</script>

<template>
  <UModal
    :close="{ onClick: () => emit('close', false) }"
    :title="`This modal was opened programmatically ${count} times`"
  >
    <template #footer>
      <div class="flex gap-2">
        <UButton color="neutral" label="Dismiss" @click="emit('close', false)" />
        <UButton label="Success" @click="emit('close', true)" />
      </div>
    </template>
  </UModal>
</template>
モーダルがクローズまたは却下されたときにcloseイベントを発行しています。closeイベントを通じて任意のデータを発行でき、そのデータがopen()の解決された値になります。Promiseが解決されるためには、イベントを発行する必要があります。

次に、アプリで使用します。

<script setup lang="ts">
import { LazyModalExample } from '#components'

const count = ref(0)

const toast = useToast()
const overlay = useOverlay()

const modal = overlay.create(LazyModalExample)

async function open() {
  const instance = modal.open({
    count: count.value
  })

  const shouldIncrement = await instance.result

  if (shouldIncrement) {
    count.value++

    toast.add({
      title: `Success: ${shouldIncrement}`,
      color: 'success',
      id: 'modal-success'
    })

    // Update the count
    modal.patch({
      count: count.value
    })
    return
  }

  toast.add({
    title: `Dismissed: ${shouldIncrement}`,
    color: 'error',
    id: 'modal-dismiss'
  })
}
</script>

<template>
  <UButton label="Open" color="neutral" variant="subtle" @click="open" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { LazyModalExample } from '#components'

const count = ref(0)

const toast = useToast()
const overlay = useOverlay()

const modal = overlay.create(LazyModalExample)

async function open() {
  const instance = modal.open({
    count: count.value
  })

  const shouldIncrement = await instance.result

  if (shouldIncrement) {
    count.value++

    toast.add({
      title: `Success: ${shouldIncrement}`,
      color: 'success',
      id: 'modal-success'
    })

    // Update the count
    modal.patch({
      count: count.value
    })
    return
  }

  toast.add({
    title: `Dismissed: ${shouldIncrement}`,
    color: 'error',
    id: 'modal-dismiss'
  })
}
</script>

<template>
  <UButton label="Open" color="neutral" variant="subtle" @click="open" />
</template>
emit('close')を出力することで、モーダルコンポーネント内でモーダルを閉じることができます。

Nestedモーダル

お互いにモーダルをネストできます。

<script setup lang="ts">
const first = ref(false)
const second = ref(false)
</script>

<template>
  <UModal v-model:open="first" title="First modal" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <UModal v-model:open="second" title="Second modal" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </UModal>
    </template>
  </UModal>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const first = ref(false)
const second = ref(false)
</script>

<template>
  <UModal v-model:open="first" title="First modal" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <UModal v-model:open="second" title="Second modal" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </UModal>
    </template>
  </UModal>
</template>

フッタースロット付き

#footerスロットを使用して、Modal本体の後にコンテンツを追加します。

<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <UModal v-model:open="open" title="Modal with footer" description="This is useful when you want a form in a Modal." :ui="{ footer: 'justify-end' }">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>

    <template #footer="{ close }">
      <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
      <UButton label="Submit" color="neutral" />
    </template>
  </UModal>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <UModal v-model:open="open" title="Modal with footer" description="This is useful when you want a form in a Modal." :ui="{ footer: 'justify-end' }">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-48" />
    </template>

    <template #footer="{ close }">
      <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
      <UButton label="Submit" color="neutral" />
    </template>
  </UModal>
</template>

Withコマンドパレット

CommandPaletteコンポーネントをModalのコンテンツ内で使用できます。

<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'modal-command-palette-users',
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  immediate: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UModal @update:open="onOpen">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </UModal>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'

const searchTerm = ref('')

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'modal-command-palette-users',
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const } })) || []
  },
  immediate: false
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true
}])

function onOpen() {
  if (!users.value?.length) {
    execute()
  }
}
</script>

<template>
  <UModal @update:open="onOpen">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </UModal>
</template>
この例ではuseLazyFetchとimmediate: falseを使用して、Modalが開いたときにのみデータをフェッチします。

API

Props

Prop Default Type
title
string
description
string
content
DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

The content of the modal.

overlay
true
boolean

Render an overlay behind the modal.

scrollable
false
boolean

When true, enables scrollable overlay mode where content scrolls within the overlay.

transition
true
boolean

Animate the modal when opening or closing.

fullscreen
false
boolean

When true, the modal will take up the full screen.

portal
true
string | false | true | HTMLElement

Render the modal in a portal.

close
true
boolean | Omit<ButtonProps, LinkPropsKeys>

Display a close button to dismiss the modal. { size: 'md', color: 'neutral', variant: 'ghost' }

closeIcon
appConfig.ui.icons.close
any

The icon displayed in the close button.

dismissible
true
boolean

When false, the modal will not close when clicking outside or pressing escape.

open
boolean

The controlled open state of the dialog. Can be binded as v-model:open.

defaultOpen
boolean

The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.

modal
true
boolean

The modality of the dialog When set to true,
interaction with outside elements will be disabled and only dialog content will be visible to screen readers.

unmountOnHide
true
boolean

When set to false, the dialog content will not be unmounted when closed, but instead hidden with CSS.
Useful for SEO or when you want to improve performance by not remounting the component on every open.

ui
{ overlay?: SlotClass; content?: SlotClass; header?: SlotClass; wrapper?: SlotClass; body?: SlotClass; footer?: SlotClass; title?: SlotClass; description?: SlotClass; close?: SlotClass; }

スロット

Slot Type
default
{ open: boolean; }
content
{ close: () => void; }
header
{ close: () => void; }
title
{}
description
{}
actions
{}
close
{ ui: object; }
body
{ close: () => void; }
footer
{ close: () => void; }

Emits

Event Type
leave
[]
after:leave
[]
enter
[]
after:enter
[]
close:prevent
[]
update:open
[value: boolean]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    modal: {
      slots: {
        overlay: 'fixed inset-0',
        content: 'bg-default divide-y divide-default flex flex-col focus:outline-none',
        header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-(--ui-header-height)',
        wrapper: '',
        body: 'flex-1 p-4 sm:p-6',
        footer: 'flex items-center gap-1.5 p-4 sm:px-6',
        title: 'text-highlighted font-semibold',
        description: 'mt-1 text-muted text-sm',
        close: 'absolute top-4 end-4'
      },
      variants: {
        transition: {
          true: {
            overlay: 'data-[state=open]:animate-[fade-in_200ms_var(--ease-out)] data-[state=closed]:animate-[fade-out_200ms_var(--ease-out)]',
            content: 'data-[state=open]:animate-[scale-in_200ms_var(--ease-out)] data-[state=closed]:animate-[scale-out_200ms_var(--ease-out)]'
          }
        },
        fullscreen: {
          true: {
            content: 'inset-0'
          },
          false: {
            content: 'w-[calc(100vw-2rem)] max-w-lg rounded-lg shadow-lg ring ring-default'
          }
        },
        overlay: {
          true: {
            overlay: 'bg-elevated/75'
          }
        },
        scrollable: {
          true: {
            overlay: 'overflow-y-auto',
            content: 'relative'
          },
          false: {
            content: 'fixed',
            body: 'overflow-y-auto'
          }
        }
      },
      compoundVariants: [
        {
          scrollable: true,
          fullscreen: false,
          class: {
            overlay: 'grid place-items-center p-4 sm:py-8'
          }
        },
        {
          scrollable: false,
          fullscreen: false,
          class: {
            content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] overflow-hidden'
          }
        }
      ]
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        modal: {
          slots: {
            overlay: 'fixed inset-0',
            content: 'bg-default divide-y divide-default flex flex-col focus:outline-none',
            header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-(--ui-header-height)',
            wrapper: '',
            body: 'flex-1 p-4 sm:p-6',
            footer: 'flex items-center gap-1.5 p-4 sm:px-6',
            title: 'text-highlighted font-semibold',
            description: 'mt-1 text-muted text-sm',
            close: 'absolute top-4 end-4'
          },
          variants: {
            transition: {
              true: {
                overlay: 'data-[state=open]:animate-[fade-in_200ms_var(--ease-out)] data-[state=closed]:animate-[fade-out_200ms_var(--ease-out)]',
                content: 'data-[state=open]:animate-[scale-in_200ms_var(--ease-out)] data-[state=closed]:animate-[scale-out_200ms_var(--ease-out)]'
              }
            },
            fullscreen: {
              true: {
                content: 'inset-0'
              },
              false: {
                content: 'w-[calc(100vw-2rem)] max-w-lg rounded-lg shadow-lg ring ring-default'
              }
            },
            overlay: {
              true: {
                overlay: 'bg-elevated/75'
              }
            },
            scrollable: {
              true: {
                overlay: 'overflow-y-auto',
                content: 'relative'
              },
              false: {
                content: 'fixed',
                body: 'overflow-y-auto'
              }
            }
          },
          compoundVariants: [
            {
              scrollable: true,
              fullscreen: false,
              class: {
                overlay: 'grid place-items-center p-4 sm:py-8'
              }
            },
            {
              scrollable: false,
              fullscreen: false,
              class: {
                content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] overflow-hidden'
              }
            }
          ]
        }
      }
    })
  ]
})

Changelog

No recent changes