Lzh on GitHub
一个对话框,可用于显示消息或请求用户输入。

用法

在 Modal 的默认插槽中使用 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 prop 设置 Modal 头部的标题。

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

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

描述

使用 description prop 设置 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 prop 自定义或隐藏 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)

使用 close-icon prop 自定义关闭按钮 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 键全局自定义此图标。

叠加层 (Overlay)

使用 overlay prop 控制 Modal 是否有叠加层。默认为 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>

过渡 (Transition)

使用 transition prop 控制 Modal 是否有动画。默认为 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>

全屏 (Fullscreen)

使用 fullscreen prop 使 Modal 全屏显示。

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

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

示例

控制打开状态

你可以通过使用 default-open prop 或 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>
在此示例中,利用 defineShortcuts,你可以通过按下 O 来切换 Modal。
这允许你将触发器移到 Modal 之外或完全移除它。

禁用 dismissal

dismissible prop 设置为 false,以防止通过点击 Modal 外部或按 escape 键关闭 Modal。当用户尝试关闭时,将发出 close:prevent 事件。

<template>
  <UModal :dismissible="false" title="Modal non-dismissible">
    <UButton label="Open" color="neutral" variant="subtle" />

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

程序化使用

你可以使用 useOverlay 可组合项以程序化方式打开 Modal。

请确保用 App 组件包裹你的应用程序,它使用 OverlayProvider 组件。

首先,创建一个将以程序化方式打开的模态组件:

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 事件发出任何数据,但是必须发出该事件才能捕获返回值。

然后,在你的应用程序中使用它:

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

const count = ref(0)

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

const modal = overlay.create(LazyModalExample, {
  props: {
    count: count.value
  }
})

async function open() {
  const instance = modal.open()

  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') 来关闭模态框。

嵌套模态框

你可以将模态框相互嵌套。

<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>

带页脚插槽

使用 #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>

带命令面板

你可以在 Modal 的内容中使用 CommandPalette 组件。

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

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  key: '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}` } })) || []
  },
  lazy: true
})

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

<template>
  <UModal>
    <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>

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.

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 | Partial<ButtonProps>

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

closeIcon

appConfig.ui.icons.close

string

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.

ui

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

Slots

Slot Type
default

{ open: boolean; }

content

{ close: () => void; }

header

{ close: () => void; }

title

{}

description

{}

actions

{}

close

{ close: () => void; ui: { overlay: (props?: Record<string, any> | undefined) => string; content: (props?: Record<string, any> | undefined) => string; header: (props?: Record<string, any> | undefined) => string; ... 5 more ...; close: (props?: Record<...> | undefined) => string; }; }

body

{ close: () => void; }

footer

{ close: () => void; }

Emits

Event Type
update:open

[value: boolean]

after:leave

[]

after:enter

[]

close:prevent

[]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    modal: {
      slots: {
        overlay: 'fixed inset-0 bg-elevated/75',
        content: 'fixed 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-16',
        wrapper: '',
        body: 'flex-1 overflow-y-auto 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_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]',
            content: 'data-[state=open]:animate-[scale-in_200ms_ease-out] data-[state=closed]:animate-[scale-out_200ms_ease-in]'
          }
        },
        fullscreen: {
          true: {
            content: 'inset-0'
          },
          false: {
            content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[calc(100vw-2rem)] max-w-lg max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] rounded-lg shadow-lg ring ring-default 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 bg-elevated/75',
            content: 'fixed 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-16',
            wrapper: '',
            body: 'flex-1 overflow-y-auto 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_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]',
                content: 'data-[state=open]:animate-[scale-in_200ms_ease-out] data-[state=closed]:animate-[scale-out_200ms_ease-in]'
              }
            },
            fullscreen: {
              true: {
                content: 'inset-0'
              },
              false: {
                content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[calc(100vw-2rem)] max-w-lg max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] rounded-lg shadow-lg ring ring-default overflow-hidden'
              }
            }
          }
        }
      }
    })
  ]
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import uiPro from '@nuxt/ui-pro/vite'

export default defineConfig({
  plugins: [
    vue(),
    uiPro({
      ui: {
        modal: {
          slots: {
            overlay: 'fixed inset-0 bg-elevated/75',
            content: 'fixed 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-16',
            wrapper: '',
            body: 'flex-1 overflow-y-auto 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_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]',
                content: 'data-[state=open]:animate-[scale-in_200ms_ease-out] data-[state=closed]:animate-[scale-out_200ms_ease-in]'
              }
            },
            fullscreen: {
              true: {
                content: 'inset-0'
              },
              false: {
                content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[calc(100vw-2rem)] max-w-lg max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] rounded-lg shadow-lg ring ring-default overflow-hidden'
              }
            }
          }
        }
      }
    })
  ]
})