UIPackage

Dialog

Vue overlay
Edit on GitHub

Free-form modal primitive — composable from `Dialog`, `DialogTrigger`, `DialogContent`, and friends. Use for forms, info cards, pickers, and any custom modal layout. For confirm/destructive prompts, prefer the prebuilt `AlertModal` shortcut.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/dialog.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/dialog

Examples

Dependencies

Used by

Files (11)

  • app/components/ui/dialog/Dialog.vue 0.6 kB
    <script setup lang="ts">
    import { computed } from 'vue'
    import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
    import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
    
    const props = defineProps<DialogRootProps>()
    const emits = defineEmits<DialogRootEmits>()
    
    const forwarded = useForwardPropsEmits(props, emits)
    
    // Merge forwarded with slotProps to ensure state is passed through
    const mergedProps = computed(() => ({
      ...forwarded.value,
    }))
    </script>
    
    <template>
      <DialogRoot v-slot="slotProps" v-bind="mergedProps">
        <slot v-bind="slotProps" />
      </DialogRoot>
    </template>
  • app/components/ui/dialog/DialogClose.vue 0.3 kB
    <script setup lang="ts">
    import type { DialogCloseProps } from 'reka-ui'
    import { DialogClose } from 'reka-ui'
    
    const props = defineProps<DialogCloseProps>()
    </script>
    
    <template>
      <DialogClose data-uipkge data-slot="dialog-close" v-bind="props">
        <slot />
      </DialogClose>
    </template>
  • app/components/ui/dialog/DialogContent.vue 2 kB
    <script setup lang="ts">
    import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { X } from 'lucide-vue-next'
    import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
    import { cn } from '@/lib/utils'
    import DialogOverlay from './DialogOverlay.vue'
    
    defineOptions({
      inheritAttrs: false,
    })
    
    const props = withDefaults(
      defineProps<DialogContentProps & { class?: HTMLAttributes['class']; showCloseButton?: boolean }>(),
      {
        showCloseButton: true,
      },
    )
    const emits = defineEmits<DialogContentEmits>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    
    const forwarded = useForwardPropsEmits(delegatedProps, emits)
    </script>
    
    <template>
      <DialogPortal>
        <DialogOverlay />
        <DialogContent
          data-uipkge
          data-slot="dialog-content"
          role="dialog"
          v-bind="{ ...$attrs, ...forwarded }"
          :class="
            cn(
              'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
              props.class,
            )
          "
        >
          <slot />
    
          <DialogClose
            v-if="showCloseButton"
            data-uipkge
            data-slot="dialog-close"
            class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
          >
            <X />
            <span class="sr-only">Close</span>
          </DialogClose>
        </DialogContent>
      </DialogPortal>
    </template>
  • app/components/ui/dialog/DialogDescription.vue 0.7 kB
    <script setup lang="ts">
    import type { DialogDescriptionProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { DialogDescription, useForwardProps } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    
    const forwardedProps = useForwardProps(delegatedProps)
    </script>
    
    <template>
      <DialogDescription
        data-uipkge
        data-slot="dialog-description"
        v-bind="forwardedProps"
        :class="cn('text-muted-foreground text-sm', props.class)"
      >
        <slot />
      </DialogDescription>
    </template>
  • app/components/ui/dialog/DialogFooter.vue 0.6 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { DialogClose } from 'reka-ui'
    import { cn } from '@/lib/utils'
    import { Button } from '@/components/ui/button'
    
    const props = withDefaults(
      defineProps<{
        class?: HTMLAttributes['class']
        showCloseButton?: boolean
      }>(),
      {
        showCloseButton: false,
      },
    )
    </script>
    
    <template>
      <div
        data-uipkge
        data-slot="dialog-footer"
        :class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
      >
        <slot />
        <DialogClose v-if="showCloseButton" as-child>
          <Button variant="outline"> Close </Button>
        </DialogClose>
      </div>
    </template>
  • app/components/ui/dialog/DialogHeader.vue 0.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    </script>
    
    <template>
      <div data-uipkge data-slot="dialog-header" :class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)">
        <slot />
      </div>
    </template>
  • app/components/ui/dialog/DialogOverlay.vue 0.7 kB
    <script setup lang="ts">
    import type { DialogOverlayProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { DialogOverlay } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    </script>
    
    <template>
      <DialogOverlay
        data-uipkge
        data-slot="dialog-overlay"
        v-bind="delegatedProps"
        :class="
          cn(
            'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-foreground/50 fixed inset-0 z-50',
            props.class,
          )
        "
      >
        <slot />
      </DialogOverlay>
    </template>
  • app/components/ui/dialog/DialogScrollContent.vue 1.8 kB
    <script setup lang="ts">
    import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { X } from 'lucide-vue-next'
    import { DialogClose, DialogContent, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    defineOptions({
      inheritAttrs: false,
    })
    
    const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
    const emits = defineEmits<DialogContentEmits>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    
    const forwarded = useForwardPropsEmits(delegatedProps, emits)
    </script>
    
    <template>
      <DialogPortal>
        <DialogOverlay
          class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-foreground/50 fixed inset-0 z-50 grid place-items-center overflow-y-auto"
        >
          <DialogContent
            :class="
              cn(
                'bg-background relative z-50 my-8 grid w-full max-w-lg gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
                props.class,
              )
            "
            v-bind="{ ...$attrs, ...forwarded }"
            @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 class="hover:bg-secondary absolute top-4 right-4 rounded-md p-0.5 transition-colors duration-200">
              <X class="h-4 w-4" />
              <span class="sr-only">Close</span>
            </DialogClose>
          </DialogContent>
        </DialogOverlay>
      </DialogPortal>
    </template>
  • app/components/ui/dialog/DialogTitle.vue 0.6 kB
    <script setup lang="ts">
    import type { DialogTitleProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { DialogTitle, useForwardProps } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    
    const forwardedProps = useForwardProps(delegatedProps)
    </script>
    
    <template>
      <DialogTitle
        data-uipkge
        data-slot="dialog-title"
        v-bind="forwardedProps"
        :class="cn('text-lg leading-none font-semibold', props.class)"
      >
        <slot />
      </DialogTitle>
    </template>
  • app/components/ui/dialog/DialogTrigger.vue 0.4 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { DialogTrigger } from 'reka-ui'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
      asChild?: boolean
      dialog?: any
    }>()
    </script>
    
    <template>
      <DialogTrigger data-uipkge data-slot="dialog-trigger" :class="props.class" :as-child="props.asChild">
        <slot />
      </DialogTrigger>
    </template>
  • app/components/ui/dialog/index.ts 0.6 kB
    export { default as Dialog } from './Dialog.vue'
    export { default as DialogClose } from './DialogClose.vue'
    export { default as DialogContent } from './DialogContent.vue'
    export { default as DialogDescription } from './DialogDescription.vue'
    export { default as DialogFooter } from './DialogFooter.vue'
    export { default as DialogHeader } from './DialogHeader.vue'
    export { default as DialogOverlay } from './DialogOverlay.vue'
    export { default as DialogScrollContent } from './DialogScrollContent.vue'
    export { default as DialogTitle } from './DialogTitle.vue'
    export { default as DialogTrigger } from './DialogTrigger.vue'

Raw manifest: https://uipkge.dev/r/vue/dialog.json