Black Friday: 50% off all products for the first 50 customers. Only 10 left. Use code
40/50 claimed
See pricing →
November 5, 2026

Quasar QDialog: Modals, Confirm Boxes and Forms

Quasar QDialog, the Dialog plugin and useDialogPluginComponent compared, with a form in a modal and a reusable confirm dialog you can call from any page.
Quasar
Vue
Components
Dialogs

Quasar QDialog is one of three ways to open a dialog in a Quasar app, and each suits a different job. The QDialog component is a modal you write into a template and open with v-model. The Dialog plugin opens a ready-made alert, confirm or prompt box from a single function call. And useDialogPluginComponent lets the plugin open a component you wrote yourself. This post covers all three with the Quasar 2.35.2 API, builds a form inside a QDialog, and ends with one reusable confirm dialog you can call from anywhere.

The Quasar QDialog component

QDialog wraps any content, usually a q-card, and shows it above the page. Its open state is a boolean v-model:

<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <q-btn color="primary" label="Show details" @click="open = true" />

  <q-dialog v-model="open">
    <q-card style="width: 420px; max-width: 90vw">
      <q-card-section class="text-h6">Order #1042</q-card-section>
      <q-card-section class="q-pt-none">
        Shipped on Monday. Tracking updates every few hours.
      </q-card-section>
      <q-card-actions align="right">
        <q-btn flat color="primary" label="Close" v-close-popup />
      </q-card-actions>
    </q-card>
  </q-dialog>
</template>

v-close-popup is a directive that closes the nearest popup (a dialog or a menu) when the element is clicked, so a Close button needs no handler. By default, a click on the backdrop or the ESC key also closes the dialog, and so does a route change.

The props you will reach for most:

PropWhat it does
persistentBackdrop click, ESC and route change no longer close it; the dialog shakes instead
no-backdrop-dismiss, no-esc-dismiss, no-route-dismissTurn off one of those three on its own
positionstandard (centered), top, right, bottom or left
maximizedFull screen, which suits forms on phones
full-width, full-heightStretch to the window's width or height
seamlessNo backdrop; the page behind stays usable
backdrop-filterA CSS backdrop filter such as 'blur(4px)' (since v2.15)

position="right" with full-height turns a dialog into a side panel, which is handy for a detail view or a filter form.

For accessibility, QDialog moves focus into itself when it opens, keeps focus from leaving it, and puts focus back on the element that opened it when it closes. Leave those defaults alone unless you have a reason (no-focus, no-refocus and allow-focus-outside exist for those cases).

A form inside a QDialog

The most common real dialog is a small form: create a project, rename a file, invite a user. Three details make it behave well: submit through q-form so validation runs, block closing while the request is in flight, and reset the fields when the dialog closes.

<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
const name = ref('')
const saving = ref(false)

async function createProject() {
  saving.value = true
  try {
    await api.createProject({ name: name.value })
    open.value = false
  }
  finally {
    saving.value = false
  }
}
</script>

<template>
  <q-btn color="primary" unelevated no-caps label="New project" @click="open = true" />

  <q-dialog v-model="open" :persistent="saving" @hide="name = ''">
    <q-card style="width: 420px; max-width: 90vw">
      <q-form @submit="createProject">
        <q-card-section class="text-h6">New project</q-card-section>

        <q-card-section class="q-pt-none">
          <q-input
            v-model="name"
            label="Project name"
            autofocus
            :rules="[val => !!val || 'Enter a name']"
          />
        </q-card-section>

        <q-card-actions align="right">
          <q-btn flat no-caps label="Cancel" :disable="saving" v-close-popup />
          <q-btn type="submit" color="primary" unelevated no-caps label="Create" :loading="saving" />
        </q-card-actions>
      </q-form>
    </q-card>
  </q-dialog>
</template>

api.createProject stands for your own call. What each piece does:

  • q-form @submit only fires when every rule passes, and the Enter key submits too.
  • :persistent="saving" stops a backdrop click or ESC from closing the dialog halfway through the request.
  • @hide runs after the dialog has closed, by any route, so the next open starts with an empty field.
  • If createProject throws, the dialog stays open, so the user doesn't lose what they typed. Show the error in the dialog (an error-message on the input, or a banner).

The Quasar dialog plugin: alert, confirm, prompt

For a plain "Are you sure?", writing a QDialog in the template is a lot of markup. The Dialog plugin builds one for you. Turn it on first: in a Quasar CLI app add 'Dialog' to framework.plugins in the Quasar config, and with nuxt-quasar-ui add it to quasar.plugins in nuxt.config.ts.

import { useQuasar } from 'quasar'

const $q = useQuasar()

function deleteProject(id: string) {
  $q.dialog({
    title: 'Delete project?',
    message: 'Its tasks and files are deleted too. This cannot be undone.',
    cancel: true,
    persistent: true,
    focus: 'cancel',
    ok: { label: 'Delete', color: 'negative', unelevated: true },
  })
    .onOk(() => api.deleteProject(id))
    .onCancel((reason) => {
      // 'cancel', 'backdrop', 'escape' or 'programmatic' (Quasar 2.28+)
    })
}

The options worth knowing:

  • cancel: true adds a Cancel button. ok and cancel also take an object of QBtn props, as above.
  • focus picks which button gets focus first: 'ok' (the default), 'cancel' or 'none'. For a destructive action, focus Cancel so a stray Enter key doesn't delete anything.
  • prompt: { model: '', type: 'text', isValid: val => val.length > 2 } turns it into a prompt, and onOk receives the typed value. options does the same with radios, checkboxes or toggles.
  • progress: true with ok: false shows a spinner you can update() and hide() from code.

The call returns a chain object: onOk, onCancel and onDismiss (which runs after either). Since Quasar 2.28, onCancel receives why the dialog closed, which helps when "closed by clicking outside" and "clicked Cancel" should do different things.

The $q.dialog call above works in a Nuxt page too; the Nuxt + Quasar setup guide covers registering the plugin there.

A reusable confirm dialog with useDialogPluginComponent

The built-in dialog only takes the options above. When you want your own layout (an icon, a colored header, an extra checkbox) and still want the one-line call, the plugin can open your component instead. The component must follow a small contract, and the useDialogPluginComponent composable handles it.

<!-- components/ConfirmDialog.vue -->
<script setup lang="ts">
import { useDialogPluginComponent } from 'quasar'

withDefaults(defineProps<{
  title: string
  message: string
  confirmLabel?: string
  destructive?: boolean
}>(), {
  confirmLabel: 'Confirm',
  destructive: false,
})

defineEmits([...useDialogPluginComponent.emits])

const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
</script>

<template>
  <q-dialog ref="dialogRef" @hide="onDialogHide">
    <q-card class="q-dialog-plugin">
      <q-card-section class="row items-start no-wrap q-gutter-x-md">
        <q-icon
          :name="destructive ? 'warning' : 'help_outline'"
          :color="destructive ? 'negative' : 'primary'"
          size="md"
        />
        <div>
          <div class="text-h6">{{ title }}</div>
          <p class="text-body2 text-grey-8 q-mb-none">{{ message }}</p>
        </div>
      </q-card-section>

      <q-card-actions align="right">
        <q-btn flat no-caps label="Cancel" @click="onDialogCancel" />
        <q-btn
          unelevated
          no-caps
          :color="destructive ? 'negative' : 'primary'"
          :label="confirmLabel"
          @click="onDialogOK()"
        />
      </q-card-actions>
    </q-card>
  </q-dialog>
</template>

The parts the plugin needs:

  • ref="dialogRef" on the q-dialog, so the plugin can show and hide it.
  • @hide="onDialogHide", bound directly. The docs warn that a wrapper like @hide="onDialogHide()" drops the event and with it the dismissal reason.
  • defineEmits([...useDialogPluginComponent.emits]).
  • onDialogOK() to settle with OK (it also hides the dialog), onDialogCancel for Cancel.

The q-dialog-plugin class gives the card the same width as the built-in plugin dialog.

Now wrap the call in a function that returns a promise, so callers can await it:

// utils/confirm.ts
import { Dialog } from 'quasar'
import ConfirmDialog from '../components/ConfirmDialog.vue'

export function confirmAction(props: {
  title: string
  message: string
  confirmLabel?: string
  destructive?: boolean
}): Promise<boolean> {
  return new Promise((resolve) => {
    Dialog.create({ component: ConfirmDialog, componentProps: props })
      .onOk(() => resolve(true))
      .onCancel(() => resolve(false))
  })
}
async function archive(project: Project) {
  const confirmed = await confirmAction({
    title: `Archive ${project.name}?`,
    message: 'Archived projects are read-only. You can restore them later.',
    confirmLabel: 'Archive',
  })
  if (confirmed)
    await api.archiveProject(project.id)
}

onCancel fires for the Cancel button, a backdrop click, ESC and a route change, so the promise always settles. Every confirm in the app now looks the same and is one await away.

If the confirm button has to wait on a request and show an error without closing, don't call onDialogOK() right away: run the request inside the component first, and call it only when the request succeeds. The Quasar docs have a full example of this "async submission" pattern.

QDialog vs Dialog plugin vs custom component

You needUse
A dialog with a form, tabs or anything bound to the page's stateq-dialog in the template
A quick alert, confirm or one-field prompt$q.dialog({ ... })
A dialog with your own layout, called from many places or from code outside componentsDialog.create({ component }) with useDialogPluginComponent

A rough rule: if the dialog's content depends on the page around it, write it in the template. If it is a question the app asks in many places, make it a plugin component.

Common questions about Quasar QDialog

How do I stop a dialog from closing when clicking outside? Add persistent, or no-backdrop-dismiss to block only the backdrop click.

Why is $q.dialog undefined? The Dialog plugin isn't registered. Add 'Dialog' to the plugins list in the Quasar config or nuxt.config.ts.

How do I make a dialog full screen on mobile only? Bind :maximized="$q.screen.lt.sm", with $q from useQuasar().

Can I close a menu and the dialog it sits in with one click? Yes. v-close-popup takes a number: v-close-popup="2" closes two popups up the chain, and -1 closes all of them.

Next step

If you would rather start from finished designs, the Dialogs Block has five dialog Components built on Quasar: activate and deactivate confirms, a success message, a team dialog and a form with a save button. The activate confirm is free: preview it, copy the code, and drop the q-card into the ConfirmDialog above.