Usage
Use aButton or any other component in the default slot of the Modal.
Then, use the#content
slot to add the content displayed when the Modal is open.
<template> <UModal> <UButton label="Open" color="neutral" variant="subtle" /> <template #content> <Placeholder class="h-48 m-4" /> </template> </UModal></template>
You can also use the#header
,#body
and#footer
slots to customize the Modal's content.
Title
Use thetitle
prop to set the title of the Modal's header.
<template> <UModal title="Modal with title"> <UButton label="Open" color="neutral" variant="subtle" /> <template #body> <Placeholder class="h-48" /> </template> </UModal></template>
Description
Use thedescription
prop to set the description of the Modal's header.
<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
Use theclose
prop to customize or hide the close button (withfalse
value) displayed in the Modal's header.
You can pass any property from theButton component to customize it.
<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
slot is used as it's a part of the header.Close Icon
Use theclose-icon
prop to customize the close buttonIcon. Defaults toi-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>
Overlay
Use theoverlay
prop to control whether the Modal has an overlay or not. Defaults totrue
.
<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
Use thetransition
prop to control whether the Modal is animated or not. Defaults totrue
.
<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
Use thefullscreen
prop to make the Modal fullscreen.
<template> <UModal fullscreen title="Modal fullscreen"> <UButton label="Open" color="neutral" variant="subtle" /> <template #body> <Placeholder class="h-full" /> </template> </UModal></template>
Examples
Control open state
You can control the open state by using thedefault-open
prop or thev-model:open
directive.
<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
, you can toggle the Modal by pressingO.Prevent closing
Set thedismissible
prop tofalse
to prevent the Modal from being closed when clicking outside of it or pressing escape.
<template> <UModal :dismissible="false" title="Modal non-dismissible"> <UButton label="Open" color="neutral" variant="subtle" /> <template #body> <Placeholder class="h-48" /> </template> </UModal></template>
Programmatic usage
You can use theuseOverlay
composable to open a Modal programatically.
App
component which uses theOverlayProvider
component.First, create a modal component that will be opened programatically:
<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
event when the modal is closed or dismissed here. You can emit any data through theclose
event, however, the event must be emitted in order to capture the return value.Then, use it in your app:
<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 shouldIncrement = await modal.open() 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 modals
You can nest modals within each other.
<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>
With footer slot
Use the#footer
slot to add content after the Modal's body.
<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> <UButton label="Cancel" color="neutral" variant="outline" @click="open = false" /> <UButton label="Submit" color="neutral" /> </template> </UModal></template>
With command palette
You can use aCommandPalette component inside the Modal's content.
<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 |
| |
description |
| |
content |
The content of the modal. | |
overlay |
|
Render an overlay behind the modal. |
transition |
|
Animate the modal when opening or closing. |
fullscreen |
|
When |
portal |
|
Render the modal in a portal. |
close |
|
Display a close button to dismiss the modal. |
closeIcon |
|
The icon displayed in the close button. |
dismissible |
|
When |
open |
The controlled open state of the dialog. Can be binded as | |
defaultOpen |
The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. | |
modal |
|
The modality of the dialog When set to |
ui |
|
Slots
Slot | Type |
---|---|
default |
|
content |
|
header |
|
title |
|
description |
|
close |
|
body |
|
footer |
|
Emits
Event | Type |
---|---|
update:open |
|
after:leave |
|
Theme
export default defineAppConfig({ ui: { modal: { slots: { overlay: 'fixed inset-0 bg-(--ui-bg-elevated)/75', content: 'fixed bg-(--ui-bg) divide-y divide-(--ui-border) 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-(--ui-text-highlighted) font-semibold', description: 'mt-1 text-(--ui-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-[calc(var(--ui-radius)*2)] shadow-lg ring ring-(--ui-border)' } } } } }})
import { defineConfig } from 'vite'import vuefrom '@vitejs/plugin-vue'import uifrom '@nuxt/ui/vite'export default defineConfig({ plugins: [ vue(), ui({ ui: { modal: { slots: { overlay: 'fixed inset-0 bg-(--ui-bg-elevated)/75', content: 'fixed bg-(--ui-bg) divide-y divide-(--ui-border) 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-(--ui-text-highlighted) font-semibold', description: 'mt-1 text-(--ui-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-[calc(var(--ui-radius)*2)] shadow-lg ring ring-(--ui-border)' } } } } } }) ]})
import { defineConfig } from 'vite'import vuefrom '@vitejs/plugin-vue'import uiProfrom '@nuxt/ui-pro/vite'export default defineConfig({ plugins: [ vue(), uiPro({ ui: { modal: { slots: { overlay: 'fixed inset-0 bg-(--ui-bg-elevated)/75', content: 'fixed bg-(--ui-bg) divide-y divide-(--ui-border) 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-(--ui-text-highlighted) font-semibold', description: 'mt-1 text-(--ui-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-[calc(var(--ui-radius)*2)] shadow-lg ring ring-(--ui-border)' } } } } } }) ]})