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.
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:
| Prop | What it does |
|---|---|
persistent | Backdrop click, ESC and route change no longer close it; the dialog shakes instead |
no-backdrop-dismiss, no-esc-dismiss, no-route-dismiss | Turn off one of those three on its own |
position | standard (centered), top, right, bottom or left |
maximized | Full screen, which suits forms on phones |
full-width, full-height | Stretch to the window's width or height |
seamless | No backdrop; the page behind stays usable |
backdrop-filter | A 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).
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.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).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.
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.
| You need | Use |
|---|---|
| A dialog with a form, tabs or anything bound to the page's state | q-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 components | Dialog.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.
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.
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.