Supports modal and non-modal modes. Focus is automatically trapped when modal. Can be controlled or uncontrolled. Manages screen reader announcements with Title andDescription components. Esc closes the component automatically. NDialog
is used to create dialog windows inside the web browser. It can be used for messages, prompts, logins, and much more.
Prop Type Default Description title
String null
The title of the dialog. description
String null
The description of the dialog. showClose
Boolean true
Show the close button.
All the props available in the
Radix Vue Dialog are also
available via its subcomponents' prop names, e.g.,
_dialog-content
,
_dialog-title
, etc. refer to
Dialog Props for more details.
< template >
< NDialog
title = "Edit Profile"
description = "Edit your profile information"
>
< template # trigger >
< NButton btn = "solid-gray" >
Open Dialog
</ NButton >
</ template >
< div class = "grid gap-4 py-4" >
< div class = "grid gap-2" >
< div class = "grid grid-cols-3 items-center gap-4" >
< NLabel for = "name" >
Name
</ NLabel >
< NInput
id = "name"
: una = " {
inputWrapper: 'col-span-2' ,
} "
/>
</ div >
< div class = "grid grid-cols-3 items-center gap-4" >
< NLabel for = "email" >
Email
</ NLabel >
< NInput
id = "email"
type = "email"
: una = " {
inputWrapper: 'col-span-2' ,
} "
/>
</ div >
< div class = "grid grid-cols-3 items-center gap-4" >
< NLabel for = "password" >
Current Password
</ NLabel >
< NInput
id = "password"
type = "password"
: una = " {
inputWrapper: 'col-span-2' ,
} "
/>
</ div >
</ div >
</ div >
< template # footer >
< NButton
btn = "solid"
label = "Save Changes"
/>
</ template >
</ NDialog >
</ template >
Copy to clipboard Prop Type Description scrollable
Boolean If true, the dialog will have a scrollable body.
< template >
< NDialog
title = "Modal Title"
description = "Here is modal with overlay scroll"
scrollable
>
< template # trigger >
< NButton btn = "solid-gray" >
Open Dialog
</ NButton >
</ template >
< div class = "grid h-300 gap-4 py-4" >
< p >
This is some placeholder content to show the scrolling behavior for modals. Instead of repeating the text in the modal, we use an inline style to set a minimum height, thereby extending the length of the overall modal and demonstrating the overflow scrolling. When content becomes longer than the height of the viewport, scrolling will move the modal as needed.
</ p >
</ div >
</ NDialog >
</ template >
Copy to clipboard Prop Type Description preventClose
Boolean If true, the dialog will not close on overlay click or escape key press.
< script setup lang = "ts" >
</ script >
< template >
< NDialog
title = "Unclosable Dialog"
: _dialogClose = " {
btn: 'solid-gray' ,
} "
prevent-close
>
< template # trigger >
< NButton btn = "solid-gray" >
Open Dialog
</ NButton >
</ template >
< div >
This dialog will stay open and not close until the close button is pressed.
</ div >
</ NDialog >
</ template >
Copy to clipboard Slot Properties Description trigger
open
The trigger button used to open the dialog default
- The body of the dialog content
- The full content of the dialog. It covers the header, body, footer, and default slots. header
- Contains the title and description slots footer
- The footer. title
- The title displayed in the dialog. description
- The description displayed below the title.
< script setup lang = "ts" >
import { useClipboard } from '@vueuse/core'
const source = ref ( 'https://unaui.com/getting-started/installation' )
const { copy , copied } = useClipboard ({ source })
</ script >
< template >
< NDialog
title = "Share Link"
description = "Anyone with this link will be able to view this project."
: _dialog-footer = " {
class: 'sm:justify-start' ,
} "
>
< template # trigger >
< NButton btn = "solid-gray" >
Open Dialog
</ NButton >
</ template >
< form
class = "flex gap-2"
@ submit . prevent = " copy (source) "
>
< NInput
v-model = " source "
leading = "i-radix-icons-link-2"
: una = " {
inputWrapper: 'w-full' ,
} "
read-only
/>
< NButton
icon
square
type = "submit"
: btn = " copied ? 'outline' : 'solid'"
: label = " ! copied ? 'i-radix-icons-copy' : 'i-radix-icons-check'"
/>
</ form >
< template # footer >
< NDialogClose >
< NButton
btn = "soft-gray"
>
Close
</ NButton >
</ NDialogClose >
</ template >
</ NDialog >
</ template >
Copy to clipboard < template >
< NDialog
title = "Edit profile"
description = "Make changes to your profile here. Click save when you're done."
: _dialog-content = " {
class: 'sm:max-w-lg grid-rows-[auto_minmax(0,1fr)_auto] p-0 max-h-90dvh' ,
} "
: _dialog-footer = " {
class: 'p-6 pt-0' ,
} "
: _dialog-header = " {
class: 'p-6 pb-0' ,
} "
>
< template # trigger >
< NButton btn = "solid-gray" >
Open Dialog
</ NButton >
</ template >
< div class = "grid gap-4 overflow-y-auto px-6 py-4" >
< div class = "h-[300dvh] flex flex-col justify-between" >
< p >
This is some placeholder content to show the scrolling behavior for modals. We use repeated line breaks to demonstrate how content can exceed minimum inner height, thereby showing inner scrolling. When content becomes longer than the predefined max-height of modal, content will be cropped and scrollable within the modal.
</ p >
< p >This content should appear at the bottom after you scroll.</ p >
</ div >
</ div >
< template # footer >
< NButton
btn = "solid"
label = "Save Changes"
/>
</ template >
</ NDialog >
</ template >
Copy to clipboard A login dialog with state which closes itself after a successful login.
< script setup lang = "ts" >
const username = ref ( '' )
const password = ref ( '' )
const loginStatus = ref < 'inactive' | 'pending' | 'success' | 'failed' >( 'inactive' )
const loginMessage = ref ( '' )
const open = ref ( false )
watch (open, () => {
// Dialog refs stay loaded. They need to be reset when opened.
username.value = ''
password.value = ''
loginStatus.value = 'inactive'
loginMessage.value = ''
})
async function sleep ( seconds : number ) {
await new Promise < void >(( resolve ) => {
setTimeout (() => {
resolve ()
}, seconds * 1000 )
})
}
async function submitLogin () {
loginStatus.value = 'pending'
loginMessage.value = ''
await sleep ( 1 )
if (username.value === 'username' && password.value === 'password' ) {
loginStatus.value = 'success'
loginMessage.value = 'Login success!'
await sleep ( 3 )
open.value = false
}
else {
loginStatus.value = 'failed'
loginMessage.value = 'Invalid Username/Password'
}
}
</ script >
< template >
< NDialog
v-model : open = " open "
title = "Login"
description = "Please log in with your username and password"
>
< template # trigger >
< NButton btn = "solid-gray" label = "Login" leading = "i-mdi:login" />
</ template >
< template # default >
< form
id = "login-form" class = "grid space-y-4"
@ submit . prevent = " submitLogin () "
>
< NFormGroup label = "Username" >
< NInput
v-model = " username "
placeholder = ""
required
: disabled = " loginStatus === 'pending' || loginStatus === 'success'"
/>
</ NFormGroup >
< NFormGroup label = "Password" >
< NInput
v-model = " password "
type = "password"
required
: disabled = " loginStatus === 'pending' || loginStatus === 'success'"
/>
< div class = "text-end" >
< NPopover >
< template # trigger >
< NButton btn = "link-muted" class = "text-xs text-muted" >
Forgot password?
</ NButton >
</ template >
< div >
< p >Your username is < span class = "font-mono" >username</ span ></ p >
< p >Your password is < span class = "font-mono" >password</ span ></ p >
</ div >
</ NPopover >
</ div >
</ NFormGroup >
</ form >
</ template >
< template # footer >
< span
v-if = " loginMessage "
class = "animate-in animate-duration-1s fade-in slide-in-from-right-4"
: class = " {
'text-green' : loginStatus === 'success' ,
'text-red' : loginStatus === 'failed' ,
} "
>
{{ loginMessage }}
</ span >
< NButton
type = "submit"
form = "login-form"
btn = "solid"
label = "Login"
leading = "i-mdi:login"
: loading = " loginStatus === 'pending'"
: disabled = " loginStatus === 'success'"
/>
</ template >
</ NDialog >
</ template >
Copy to clipboard A dialog whose overlay blurs the background content.
< template >
< NDialog
title = "Blurring background"
: una = " { dialogOverlay: 'backdrop-blur' } "
>
< template # trigger >
< NButton btn = "solid-gray" label = "Open Dialog" />
</ template >
< div >
The background of this dialog is blurred. This can be used to protect sensitive information or hide certain types of images.
</ div >
</ NDialog >
</ template >
Copy to clipboard import type {
DialogCloseProps,
DialogContentProps,
DialogDescriptionProps,
DialogRootProps,
DialogTitleProps,
} from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'
export interface NDialogProps extends DialogRootProps {
title ?: string
description ?: string
scrollable ?: boolean
showClose ?: boolean
preventClose ?: boolean
// sub-components
_dialogTitle ?: NDialogTitleProps
_dialogDescription ?: NDialogDescriptionProps
_dialogHeader ?: NDialogHeaderProps
_dialogFooter ?: NDialogFooterProps
_dialogOverlay ?: NDialogOverlayProps
_dialogContent ?: NDialogContentProps
_dialogClose ?: NDialogCloseProps
una ?: NDialogUnaProps
}
interface BaseExtensions {
class ?: HTMLAttributes [ 'class' ]
}
export interface NDialogTitleProps extends DialogTitleProps , BaseExtensions {
una ?: NDialogUnaProps [ 'dialogTitle' ]
}
export interface NDialogDescriptionProps extends DialogDescriptionProps , BaseExtensions {
una ?: NDialogUnaProps [ 'dialogDescription' ]
}
export interface NDialogContentProps extends DialogContentProps , BaseExtensions , Pick < NDialogProps , '_dialogOverlay' | '_dialogClose' | 'preventClose' | 'showClose' > {
onCloseAutoFocus ?: ( event : any ) => void
onEscapeKeyDown ?: ( event : KeyboardEvent ) => void
onInteractOutside ?: ( event : any ) => void
onOpenAutoFocus ?: ( event : any ) => void
onPointerDownOutside ?: ( event : any ) => void
una ?: NDialogUnaProps [ 'dialogContent' ]
}
export interface NDialogOverlayProps extends BaseExtensions , Pick < NDialogProps , 'scrollable' > {
una ?: NDialogUnaProps [ 'dialogOverlay' ]
}
export interface NDialogHeaderProps extends BaseExtensions {
una ?: NDialogUnaProps [ 'dialogHeader' ]
}
export interface NDialogFooterProps extends BaseExtensions {
una ?: NDialogUnaProps [ 'dialogFooter' ]
}
export interface NDialogCloseProps extends DialogCloseProps , NButtonProps {
}
export interface NDialogUnaProps {
dialogTitle ?: HTMLAttributes [ 'class' ]
dialogDescription ?: HTMLAttributes [ 'class' ]
dialogHeader ?: HTMLAttributes [ 'class' ]
dialogFooter ?: HTMLAttributes [ 'class' ]
dialogOverlay ?: HTMLAttributes [ 'class' ]
dialogContent ?: HTMLAttributes [ 'class' ]
}
Copy to clipboard < script setup lang = "ts" >
import type { NDialogProps } from '../../../types'
import { reactivePick } from '@vueuse/core'
import { DialogRoot, type DialogRootEmits, DialogTrigger, useForwardPropsEmits } from 'radix-vue'
import DialogContent from './DialogContent.vue'
import DialogDescription from './DialogDescription.vue'
import DialogFooter from './DialogFooter.vue'
import DialogHeader from './DialogHeader.vue'
import DialogScrollContent from './DialogScrollContent.vue'
import DialogTitle from './DialogTitle.vue'
defineOptions ({
inheritAttrs: false ,
})
const props = withDefaults ( defineProps < NDialogProps >(), {
showClose: true ,
})
const emits = defineEmits < DialogRootEmits >()
const rootProps = reactivePick (props, [
'open' ,
'defaultOpen' ,
'modal' ,
])
const rootPropsEmits = useForwardPropsEmits (rootProps, emits)
</ script >
< template >
< DialogRoot v-slot = " { open } " v-bind = " rootPropsEmits " >
< DialogTrigger as-child >
< slot name = "trigger" : open / >
</ DialogTrigger >
< component
: is = " ! scrollable ? DialogContent : DialogScrollContent "
v-bind = " _dialogContent "
: _dialog-overlay
: _dialog-close
: una
: scrollable
: show-close
: prevent-close
: aria-describedby = " props.description ? 'dialog-description' : undefined "
>
< slot name = "content" >
< DialogHeader
v-if = " props.title || props.description || $slots.header "
v-bind = " _dialogHeader "
: una
>
< slot name = "header" >
< DialogTitle
v-if = " props.title "
v-bind = " _dialogTitle "
: una
>
< slot name = "title" >
{{ title }}
</ slot >
</ DialogTitle >
< DialogDescription
v-if = " props.description "
v-bind = " _dialogDescription "
: una
>
< slot name = "description" >
{{ description }}
</ slot >
</ DialogDescription >
</ slot >
</ DialogHeader >
<!-- body -->
< slot / >
< DialogFooter
v-if = " $slots.footer "
v-bind = " _dialogFooter "
: una
>
< slot name = "footer" / >
</ DialogFooter >
</ slot >
</ component >
</ DialogRoot >
</ template >
Copy to clipboard < script setup lang = "ts" >
import type { NDialogTitleProps } from '../../../types'
import { reactiveOmit } from '@vueuse/core'
import { DialogTitle, useForwardProps } from 'radix-vue'
import { cn } from '../../../utils'
const props = defineProps < NDialogTitleProps >()
const delegatedProps = reactiveOmit (props, 'class' )
const forwardedProps = useForwardProps (delegatedProps)
</ script >
< template >
< DialogTitle
v-bind = " forwardedProps "
: class = " cn ( 'dialog-title' , props.class) "
>
< slot / >
</ DialogTitle >
</ template >
Copy to clipboard < script setup lang = "ts" >
import type { NDialogDescriptionProps } from '../../../types'
import { reactiveOmit } from '@vueuse/core'
import { DialogDescription, useForwardProps } from 'radix-vue'
import { cn } from '../../../utils'
const props = defineProps < NDialogDescriptionProps >()
const delegatedProps = reactiveOmit (props, 'class' )
const forwardedProps = useForwardProps (delegatedProps)
</ script >
< template >
< DialogDescription
v-bind = " forwardedProps "
: class = " cn (
'dialog-description' ,
props.una?.dialogDescription,
props.class,
) "
>
< slot / >
</ DialogDescription >
</ template >
Copy to clipboard < script setup lang = "ts" >
import type { NDialogHeaderProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps < NDialogHeaderProps >()
</ script >
< template >
< div
: class = " cn (
'dialog-header' ,
props.una?.dialogHeader,
props.class,
) "
>
< slot / >
</ div >
</ template >
Copy to clipboard < script setup lang = "ts" >
import type { NDialogFooterProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps < NDialogFooterProps >()
</ script >
< template >
< div
: class = " cn (
'dialog-footer' ,
props.una?.dialogFooter,
props.class,
) "
>
< slot / >
</ div >
</ template >
Copy to clipboard < script setup lang = "ts" >
import type { NDialogCloseProps } from '../../../types'
import { reactiveOmit } from '@vueuse/core'
import { DialogClose } from 'radix-vue'
import { cn } from '../../../utils'
import Button from '../Button.vue'
const props = withDefaults ( defineProps < NDialogCloseProps >(), {
btn: 'text-muted' ,
square: '2em' ,
icon: true ,
label: 'i-close' ,
})
const delegatedProps = reactiveOmit (props, 'class' )
</ script >
< template >
< DialogClose
as-child
>
< slot >
< Button
tabindex = "-1"
v-bind = " delegatedProps "
: class = " cn ( 'dialog-close' , props.class) "
>
< template v-for = " (_, name) in $slots " #[name]= " slotData " >
< slot : name = " name " v-bind = " slotData " / >
</ template >
</ Button >
</ slot >
</ DialogClose >
</ template >
Copy to clipboard < script lang = "ts" setup >
import type { NDialogOverlayProps } from '../../../types'
import { DialogOverlay } from 'radix-vue'
import { cn } from '../../../utils'
const props = defineProps < NDialogOverlayProps >()
</ script >
< template >
< DialogOverlay
: class = " cn (
! props.scrollable ? 'dialog-overlay' : 'dialog-scroll-overlay' ,
props.una?.dialogOverlay,
props.class,
) "
>
< slot / >
</ DialogOverlay >
</ template >
Copy to clipboard < script setup lang = "ts" >
import type { NDialogContentProps } from '../../../types'
import { reactiveOmit } from '@vueuse/core'
import {
DialogContent,
type DialogContentEmits,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { cn } from '../../../utils'
import DialogClose from './DialogClose.vue'
import DialogOverlay from './DialogOverlay.vue'
defineOptions ({
inheritAttrs: false ,
})
const props = withDefaults ( defineProps < NDialogContentProps >(), {
showClose: true ,
})
const emits = defineEmits < DialogContentEmits >()
const delegatedProps = reactiveOmit (props, [ 'class' , '_dialogOverlay' , '_dialogClose' ])
const forwarded = useForwardPropsEmits (delegatedProps, emits)
</ script >
< template >
< DialogPortal >
< DialogOverlay
v-bind = " _dialogOverlay "
: una
/>
< DialogContent
v-bind = " { ... forwarded, ... $attrs } "
: class = " cn (
'dialog-content' ,
props.una?.dialogContent,
props.class,
) "
@ interact-outside = " event => {
if (preventClose) return event. preventDefault ()
} "
@ escape-key-down = " event => {
if (preventClose) return event. preventDefault ()
} "
>
< slot / >
< DialogClose
v-if = " showClose "
v-bind = " _dialogClose "
/>
</ DialogContent >
</ DialogPortal >
</ template >
Copy to clipboard < script setup lang = "ts" >
import type { NDialogContentProps } from '../../../types'
import { reactiveOmit } from '@vueuse/core'
import {
DialogContent,
type DialogContentEmits,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { cn } from '../../../utils'
import DialogClose from './DialogClose.vue'
import DialogOverlay from './DialogOverlay.vue'
defineOptions ({
inheritAttrs: false ,
})
const props = withDefaults ( defineProps < NDialogContentProps >(), {
showClose: true ,
})
const emits = defineEmits < DialogContentEmits >()
const delegatedProps = reactiveOmit (props, [ 'class' , '_dialogOverlay' , '_dialogClose' ])
const forwarded = useForwardPropsEmits (delegatedProps, emits)
</ script >
< template >
< DialogPortal >
< DialogOverlay
v-bind = " _dialogOverlay "
: una
scrollable
>
< DialogContent
v-bind = " { ... forwarded, ... $attrs } "
: class = " cn (
'dialog-scroll-content' ,
props.una?.dialogContent,
props.class,
) "
@ 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 ();
}
} "
>
< slot / >
< DialogClose
v-if = " showClose "
v-bind = " _dialogClose "
/>
</ DialogContent >
</ DialogOverlay >
</ DialogPortal >
</ template >
Copy to clipboard