Dialog

A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

Examples

Basic

PropDefaultTypeDescription
title-stringThe title of the dialog.
description-stringThe description of the dialog.
showClosetruebooleanShow the close button.
defaultOpenfalsebooleanThe open state of the dialog when it is initially rendered. Use when you do not need to control its open state.
modaltruebooleanThe 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.
open-booleanThe controlled open state of the dialog. Can be binded as v-model:open.
Preview
Code

Scrollable Content

PropDefaultTypeDescription
scrollablefalsebooleanIf true, the dialog will have a scrollable body.
Preview
Code

Prevent Closing

PropDefaultTypeDescription
preventClose-booleanIf true, the dialog will not close on overlay click or escape key press.
Preview
Code

Slots

NamePropsDescription
default-The trigger slot.
content-The content slot.
triggeropenThe trigger button used to open the dialog.
header-Contains the title and description slots.
footer-The footer.
title-The title displayed in the dialog.
description-The description displayed below the title.

Custom Close Button

Preview
Code

Scrollable Body

Preview
Code

Login Prompt

A login dialog with state which closes itself after a successful login.

Preview
Code

Blurred Background

A dialog whose overlay blurs the background content.

Preview
Code

Presets

shortcuts/dialog.ts
type KbdPrefix = 'dialog'

export const staticDialog: Record<`${KbdPrefix}-${string}` | KbdPrefix, string> = {
  // base
  'dialog': '',

  // sub-components
  'dialog-overlay': 'fixed inset-0 z-50 bg-black/80',
  'dialog-content': 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-base bg-base p-6 shadow-lg duration-200 sm:rounded-lg',

  'dialog-scroll-overlay': 'fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80',
  'dialog-scroll-content': 'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-base bg-base p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',

  'dialog-header': 'flex flex-col gap-y-1.5 text-center sm:text-left',
  'dialog-title': 'text-lg font-semibold leading-none tracking-tight',
  'dialog-description': 'text-sm text-muted',
  'dialog-close': 'absolute right-4 top-4',
  'dialog-footer': 'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
}

export const dynamicDialog: [RegExp, (params: RegExpExecArray) => string][] = [
  // dynamic preset
]

export const dialog = [
  ...dynamicDialog,
  staticDialog,
]

Props

types/dialog.ts
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']
}

Components

Dialog.vue
DialogTitle.vue
DialogDescription.vue
DialogHeader.vue
DialogFooter.vue
DialogClose.vue
DialogOverlay.vue
DialogContent.vue
DialogScrollContent.vue
<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>