Modal

对话框GitHub
一个可用于显示消息或请求用户输入的对话窗口。

用法

在模态框的默认插槽中使用Button或任何其他组件。

然后,使用#content插槽添加模态框打开时显示的内容。

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

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

你还可以使用#header#body#footer插槽来自定义模态框的内容。

标题

使用title属性设置模态框标题。

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

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

描述

使用description属性设置模态框标题描述。

<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属性自定义或隐藏模态框标题中显示的关闭按钮(使用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 属性自定义关闭按钮的 图标。默认为 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属性控制模态框是否动画。默认为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属性控制模态框是否阻止与外部内容的交互。默认为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属性控制模态框是否可以通过点击外部或按Esc键关闭。默认为true

当用户尝试关闭时,将发出一个close:prevent事件。
你可以将modal: falsedismissible: false结合使用,使模态框的背景可交互而不关闭它。
<template>
  <UModal :dismissible="false" title="Modal non-dismissible">
    <UButton label="Open" color="neutral" variant="subtle" />

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

可滚动 4.2+

使用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>
有一个已知问题在某些操作系统上,点击滚动条可能会意外关闭对话框。

全屏

使用fullscreen属性使模态框全屏。

<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键来切换模态框。
这允许你将触发器移出模态框或完全删除它。

程序化使用

你可以使用useOverlay组合式函数以程序化方式打开模态框。

请确保使用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)

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

嵌套模态框

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

<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插槽在模态框主体后添加内容。

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

带命令面板

你可以在模态框内容中使用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

属性

属性默认值类型
titlestring
descriptionstring
内容DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

模态框的内容。

叠加层trueboolean

在模态框后面渲染一个叠加层。

可滚动falseboolean

当为true时,启用可滚动覆盖模式,其中内容在覆盖层内滚动。

过渡动画trueboolean

在打开或关闭时为模态框添加动画效果。

全屏falseboolean

当为 true 时,模态框将占据整个屏幕。

portaltruestring | false | true | HTMLElement

在传送门中渲染模态框。

closetrueboolean | Omit<ButtonProps, LinkPropsKeys>

显示关闭按钮以关闭模态框。{ size: 'md', color: 'neutral', variant: 'ghost' }

closeIconappConfig.ui.icons.closeany

关闭按钮中显示的图标。

可关闭的trueboolean

当为 false 时,点击外部或按下 Escape 键时模态框将不会关闭。

openboolean

对话框的受控打开状态。可以绑定为 v-model:open

defaultOpenboolean

对话框初始渲染时的打开状态。当您不需要控制其打开状态时使用。

modaltrueboolean

对话框的模态性。当设置为 true 时,
与外部元素的交互将被禁用,并且只有对话框内容对屏幕阅读器可见。

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

插槽

插槽类型
default{ open: boolean; }
内容{ close: () => void; }
页头{ close: () => void; }
title{}
description{}
actions{}
close{ ui: object; }
主体{ close: () => void; }
页脚{ close: () => void; }

事件

事件类型
after:leave[]
after:enter[]
close:prevent[]
update:open[value: boolean]

主题

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-16',
        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_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: '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-16',
            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_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: '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'
              }
            }
          ]
        }
      }
    })
  ]
})

更新日志

184ea— chore: 减少类型冗余,通过省略操作按钮中的链接属性

dd81d— feat: 添加 data-slot 属性 (#5447)

24089— feat: 添加scrollable属性 (#5306)

63c0a— feat: 在使用的 slot prop 中暴露 ui (#5207)

80994— 修复:移除关闭自动对焦阻止 (#5191)

61b60— 功能:允许传递组件而不是名称 (#4766)

5cb65— 特性:导入 @nuxt/ui-pro 组件

81569— feat: 添加actions插槽 (#4358)

be41a— 修复:移除按钮默认md尺寸 (#4357)

150b3— 修复:在closeAutoFocus上不发出close:prevent

5835e— 新增:在插槽中添加close方法 (#4219)

d9e9f— feat: 添加after:enter事件 (#4187)

f4864— 新增:添加close:prevent事件 (#3958)

29fa4— 特性:添加全局 portal 属性 (#3688)

39c86— fix:@nuxt/module-builder 升级后重构类型(#3855)

b9983— 修复:改进泛型类型 (#3331)

5dec0— feat: 处理 content 属性中的事件

f4c41— 修复:防止不必要的 close 实例化