Vue Dialog Component

Dialog

Vue Dialog component built on the native <dialog> element — top layer rendering, blurred native backdrop, focus trapping, and accessible semantics out of the box.

Available in Other JavaScript Frameworks

CoreUI Vue Dialog Component is also available for Bootstrap and React. Explore framework-specific implementations below:

Overview

Added in v6.0.0

<CDialog> renders a popup on the native <dialog> element, so accessibility, focus management and the backdrop come from the browser rather than from a custom implementation. It carries the look Bootstrap 6 introduces under the same name — a centered floating panel over a blurred backdrop.

Dialog shares its behavior with Modal. Modal keeps the classic look and the v5 vocabulary; Dialog carries the new look and the dialog-* classes. Pick one — they are independent components, and Modal is deprecated for removal in v7.

What the native element provides:

  • Modal or non-modal — modal (default true) promotes the dialog to the top layer with a backdrop and focus trapping; :modal="false" leaves the page interactive.
  • A built-in backdrop through the ::backdrop pseudo-element. backdrop="static" locks clicks outside; :backdrop="false" suppresses it visually.
  • Escape handling closes the dialog; :keyboard="false" disables it.
  • Accessibility — focus is trapped inside a modal dialog and returned to the trigger on close, with no role, aria-modal, aria-hidden or tabindex in the markup.
  • Animated open and close — the exit transition, the ::backdrop fade included, plays in every browser, because the dialog stays in the top layer until the transition ends.

Example

Toggle a dialog by clicking the button below.

vue
<template>
  <CButton theme="primary" @click="show = true">Launch dialog</CButton>
  <CDialog v-model:show="show">
    <CDialogHeader>
      <CDialogTitle>Dialog title</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>Woohoo, you are reading this text in a dialog!</CDialogBody>
    <CDialogFooter>
      <CButton theme="secondary" @click="show = false">Close</CButton>
      <CButton theme="primary">Save changes</CButton>
    </CDialogFooter>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import {
  CButton,
  CDialog,
  CDialogBody,
  CDialogFooter,
  CDialogHeader,
  CDialogTitle,
} from '@coreui/vue'

const show = ref(false)
</script>

Slide animations

Dialogs fade in and out by default. animation picks a different entry: "slide-down" comes in from above, "slide-up" from below — and the exit reverses it. The value names the variant rather than the direction, so further ones are added to the stylesheet without touching the prop.

vue
<template>
  <div class="hstack gap-3">
    <CButton theme="primary" @click="open('slide-down')">Slide down</CButton>
    <CButton theme="primary" @click="open('slide-up')">Slide up</CButton>
  </div>
  <!-- The variant is held past the close, or the exit would reverse a different entry. -->
  <CDialog v-model:show="show" :animation="animation">
    <CDialogHeader>
      <CDialogTitle>{{ animation }}</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>The exit reverses the entry.</CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const animation = ref('slide-down')
const show = ref(false)

const open = (value) => {
  animation.value = value
  show.value = true
}
</script>

Static backdrop

With backdrop="static" the dialog does not close on a click outside — it plays a scale bounce instead.

vue
<template>
  <CButton theme="primary" @click="show = true">Launch static backdrop dialog</CButton>
  <CDialog v-model:show="show" backdrop="static">
    <CDialogHeader>
      <CDialogTitle>Dialog title</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>
      Clicking outside plays a bounce instead of closing. Escape still closes it.
    </CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const show = ref(false)
</script>

Backdrop

The backdrop reads three tokens from :root — --cui-backdrop-color, --cui-backdrop-opacity and --cui-backdrop-blur — and every overlay component paints it the same way. A dialog defaults to a dark scrim; the drawer defaults to a translucent tint of the page. Pick the other look with the backdrop-variant prop — a backdrop preset on the component: translucent here, scrim on a drawer; :backdrop="false" on either drops the layer altogether.

vue
<template>
  <CButton theme="primary" @click="show = true">Launch dialog with a translucent backdrop</CButton>
  <CDialog v-model:show="show" backdrop-variant="translucent">
    <CDialogHeader>
      <CDialogTitle>Translucent backdrop</CDialogTitle>
    </CDialogHeader>
    <CDialogBody
      >The page behind stays readable through a light tint instead of a dark scrim.</CDialogBody
    >
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const show = ref(false)
</script>

Scrolling long content

Add scrollable to scroll the dialog body while the header and footer stay put.

vue
<template>
  <CButton theme="primary" @click="show = true">Launch scrollable dialog</CButton>
  <CDialog v-model:show="show" scrollable>
    <CDialogHeader>
      <CDialogTitle>Scrollable body</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>
      <p v-for="index in 20" :key="index">
        Placeholder paragraph {{ index }} — the body scrolls while the header stays put.
      </p>
    </CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const show = ref(false)
</script>

Toggle between dialogs

A trigger inside an open dialog can swap to another one — the backdrop stays up, so there is no flash between them. Drive both from one piece of state rather than closing the first and opening the second.

vue
<template>
  <CButton theme="primary" @click="open = 'first'">Open first dialog</CButton>

  <CDialog :show="open === 'first'" @update:show="(show) => !show && (open = undefined)">
    <CDialogHeader>
      <CDialogTitle>First dialog</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>
      <CButton theme="primary" @click="open = 'second'">Open second dialog</CButton>
    </CDialogBody>
  </CDialog>

  <CDialog :show="open === 'second'" @update:show="(show) => !show && (open = undefined)">
    <CDialogHeader>
      <CDialogTitle>Second dialog</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>
      <CButton theme="secondary" @click="open = 'first'">Back to the first</CButton>
    </CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const open = ref()
</script>

Non-modal dialogs

Set :modal="false" to show the dialog without the top layer — no backdrop, no focus trap, and the rest of the page stays interactive.

vue
<template>
  <CButton theme="primary" @click="show = true">Launch non-modal dialog</CButton>
  <CDialog v-model:show="show" :modal="false">
    <CDialogHeader>
      <CDialogTitle>Non-modal</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>
      No backdrop, no focus trap — the rest of the page stays interactive and scrollable.
    </CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const show = ref(false)
</script>

Optional sizes

Dialogs have three optional sizes.

SizePropDialog max-width
Smallsize="sm"280px
Default—500px
Largesize="lg"800px
Extra largesize="xl"1140px
vue
<template>
  <div class="hstack gap-3">
    <CButton v-for="value in sizes" :key="value" theme="primary" @click="open(value)">
      {{ value }} dialog
    </CButton>
  </div>
  <CDialog v-model:show="show" :size="size">
    <CDialogHeader>
      <CDialogTitle>Size {{ size }}</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>Placeholder content for the dialog body.</CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const sizes = ['sm', 'lg', 'xl']
const size = ref('lg')
const show = ref(false)

const open = (value) => {
  size.value = value
  show.value = true
}
</script>

Fullscreen dialog

Add fullscreen for a dialog that covers the viewport. Pass a breakpoint instead — sm, md, lg, xl or 2xl — to cover it only below that width and keep the floating panel above it.

vue
<template>
  <CButton theme="primary" @click="show = true">Fullscreen dialog</CButton>
  <CDialog v-model:show="show" fullscreen>
    <CDialogHeader>
      <CDialogTitle>Fullscreen</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>The dialog covers the entire viewport.</CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const show = ref(false)
</script>

Dark variant

Add dark and the dialog flips the tokens its own surface reads, so the dialog and everything in it — including the close button — turns dark on a light page.

vue
<template>
  <CButton theme="primary" @click="show = true">Open dark dialog</CButton>
  <CDialog v-model:show="show" dark>
    <CDialogHeader>
      <CDialogTitle>Dark dialog</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>The surface, the text and the dividers all read the flipped tokens.</CDialogBody>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import { CButton, CDialog, CDialogBody, CDialogHeader, CDialogTitle } from '@coreui/vue'

const show = ref(false)
</script>

Usage with v-model

Bind v-model:show and the dialog renders exactly what your state says — dismiss gestures only emit update:show, and nothing closes until you flip the prop. That makes confirm-before-close possible: instead of feeding a close request straight back (which is what v-model:show does), ask first.

vue
<template>
  <CButton theme="primary" @click="show = true">Launch dialog</CButton>
  <CDialog :show="show" @update:show="handleShowChange">
    <CDialogHeader>
      <CDialogTitle>Unsaved changes</CDialogTitle>
    </CDialogHeader>
    <CDialogBody>
      {{
        confirming
          ? 'Close without saving? Your changes will be lost.'
          : 'Try to close this dialog with Escape or a click on the backdrop — it will ask first.'
      }}
    </CDialogBody>
    <CDialogFooter>
      <template v-if="confirming">
        <CButton theme="secondary" @click="confirming = false">Keep editing</CButton>
        <CButton theme="danger" @click="close">Discard and close</CButton>
      </template>
      <CButton v-else theme="primary" @click="close">Save and close</CButton>
    </CDialogFooter>
  </CDialog>
</template>

<script setup>
import { ref } from 'vue'
import {
  CButton,
  CDialog,
  CDialogBody,
  CDialogFooter,
  CDialogHeader,
  CDialogTitle,
} from '@coreui/vue'

const show = ref(false)
const confirming = ref(false)

const handleShowChange = (nextShow) => {
  if (!nextShow) {
    confirming.value = true
    return
  }

  show.value = nextShow
}

const close = () => {
  confirming.value = false
  show.value = false
}
</script>

Use it when closing has consequences — unsaved form input, an interrupted upload — or when the open state belongs to something else, like a route.

Initial open state

Give the dialog a starting point and let it close itself — dismiss gestures are handled internally.

<CDialog show>

update:show fires either way whenever the dialog requests an open-state change — a dismiss gesture (Escape, a click on the backdrop) or any internal toggle — so you can watch the state without taking it over. The deprecated visible prop keeps its old behavior and is ignored when show is set.

Accessibility

The native <dialog> carries the semantics: a modal dialog traps focus, marks the rest of the page inert and returns focus to the trigger on close, and it is exposed as a dialog to assistive technology without any ARIA attribute of ours. Give the dialog an accessible name by pointing aria-labelledby at your <CDialogTitle>.

API

Check out the documentation below for a comprehensive guide to all the props you can use with the components mentioned here.