Vue.js Dialogs Modals FAQ & Answers
36 expert Vue.js Dialogs Modals answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
36 questionsUse promise-based dialog pattern for reusable, async-friendly confirmations. Pattern: dialog function returns Promise that resolves with user choice. Implementation with vue3-promise-dialog: Create ConfirmDialog.vue with props (message, confirmText, cancelText) and two buttons that call close(true) or close(false). Register: app.use(PromiseDialog). Usage: const confirmed = await openDialog(ConfirmDialog, { message: 'Delete item?' }); if (confirmed) { deleteItem(); }. Benefits: (1) Call from any JS/TS file (not just components), (2) Async/await syntax (cleaner than callbacks), (3) TypeScript type safety for props and return values, (4) Single dialog instance reused across app. Alternative: create global dialog service with provide/inject, but promise pattern is more intuitive. Renderless design: separate logic (promise handling) from presentation (styling) for maximum reusability across different UI frameworks.
Use vue3-promise-dialog library for type-safe promise-based dialogs. Setup: npm install vue3-promise-dialog, then in main.ts: import { PromiseDialog } from 'vue3-promise-dialog'; app.use(PromiseDialog);. Create dialog component with TypeScript props interface: interface Props { title: string; message: string; }; const props = defineProps
Custom dialogs provide superior UX and functionality over native alert(). Key benefits: (1) Non-blocking: native alert() blocks JavaScript execution and UI rendering, custom dialogs use async/await without blocking, (2) Customizable: full control over styling, branding, animations, layout vs fixed browser chrome, (3) Accessibility: add ARIA attributes, focus management, keyboard navigation, screen reader support, (4) Rich content: include forms, images, complex HTML vs plain text only, (5) Consistent UX: same look across all browsers/OS vs native browser styling, (6) Better mobile experience: responsive design, touch-friendly buttons vs tiny native buttons, (7) Testable: easy to unit test custom components vs difficulty testing native alerts. Technical advantages: promise-based API (cleaner code), can programmatically close, support multiple simultaneous dialogs, integrate with state management (Pinia/Vuex). Native alert() only acceptable for quick debugging, never production code.
Implement dialog queue system to prevent multiple overlapping modals. Strategy 1: Single dialog instance with queue. Use reactive queue: const dialogQueue = ref<DialogConfig[]>([]); const currentDialog = computed(() => dialogQueue.value[0]); function openDialog(config) { dialogQueue.value.push(config); } function closeDialog() { dialogQueue.value.shift(); }. Only render when currentDialog exists. Strategy 2: Disable trigger buttons when dialog open. Track open state: const isDialogOpen = ref(false);
Accessible confirmation dialogs must support standard keyboard shortcuts per WCAG 2.1 guidelines. Required shortcuts: (1) Escape key: close dialog (most critical), (2) Tab/Shift+Tab: cycle focus within dialog (focus trap), (3) Enter key: activate focused button (typically 'Confirm'), (4) Space key: activate focused button. Implementation with Vue 3 Composition API: onMounted(() => { window.addEventListener('keydown', handleKeydown); }); onUnmounted(() => { window.removeEventListener('keydown', handleKeydown); }); const handleKeydown = (e: KeyboardEvent) => { if (e.key === 'Escape') close(false); if (e.key === 'Enter' && focusedElement === confirmButton) confirm(); }. Best practices: (1) Focus 'Cancel' button by default (safer, prevents accidental confirmation), (2) Highlight focused button visibly (outline, background color), (3) Arrow keys for button navigation (left/right). Avoid: requiring mouse for any action, keyboard traps user cannot escape, no visible focus indicators.
Props and slots serve different purposes in modal components. Props: pass configuration data (title, size, closable, variant), typically simple types (string, boolean, number). Example:
Teleport renders component content outside current DOM hierarchy, essential for modals to avoid CSS stacking issues. Basic implementation:
Z-index stacking context determines layering order of positioned elements. New stacking contexts created by: position (absolute/relative/fixed) + z-index, transform, opacity < 1, filter, will-change. Problem: z-index only works within same stacking context, child with z-index: 9999 cannot appear above parent's sibling with z-index: 2. Modal solution: use Teleport to move modal outside parent stacking contexts:
Focus trap keeps keyboard navigation within modal, essential for accessibility. Solution: use focus-trap library with VueUse's useFocusTrap composable. Installation: npm install focus-trap. Implementation: import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'; const modalRef = ref(); const { activate, deactivate } = useFocusTrap(modalRef); onMounted(() => activate()); onUnmounted(() => deactivate());. How it works: (1) Finds all focusable elements (buttons, inputs, links) within modal, (2) Tab from last element returns to first, (3) Shift+Tab from first returns to last, (4) Escape key deactivates trap. Manual implementation: const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const focusable = modalRef.value.querySelectorAll(focusableSelector); document.addEventListener('keydown', (e) => { if (e.key === 'Tab') { /* cycle through focusable */ } });. Best practices: (1) Focus first interactive element on open (close button or primary action), (2) Return focus to trigger element on close, (3) Use aria-modal="true" to tell screen readers only modal is accessible.
Accessible modals require specific ARIA attributes per WCAG 2.1 guidelines. Required attributes: (1) role="dialog" or role="alertdialog" on modal container (alertdialog for critical confirmations), (2) aria-modal="true" tells screen readers only modal content is accessible (hides background), (3) aria-labelledby="title-id" references modal title element ID, (4) aria-describedby="desc-id" references description/content element ID. Example:
Delete User
This action cannot be undone.
Backdrop click should close modal but requires careful event handling to prevent bugs. Pattern: listen for click on overlay (backdrop), not modal content. Implementation: <div class="overlay" @click="handleOverlayClick"><div class="modal" @click.stop>Content
Use Vue 3 Composition API with onMounted/onUnmounted for proper event listener cleanup. Pattern: onMounted(() => { window.addEventListener('keydown', handleKeydown); }); onUnmounted(() => { window.removeEventListener('keydown', handleKeydown); }); const handleKeydown = (e: KeyboardEvent) => { if (e.key === 'Escape') close(); }. Key modifiers in templates: @keydown.enter="submit", @keydown.esc="close", @keydown.ctrl.s.prevent="save" (Ctrl+S). System modifiers: .ctrl, .alt, .shift, .meta (Cmd on Mac). Exact modifier: @keydown.ctrl.exact prevents trigger when other keys pressed. VueUse composables: import { onKeyStroke } from '@vueuse/core'; onKeyStroke('Escape', () => close()); automatically handles cleanup. Global vs component listeners: window.addEventListener for app-wide shortcuts, @keydown on specific elements for component-scoped. Performance: use event.key (modern) not event.keyCode (deprecated). Prevent default: e.preventDefault() stops browser default behavior. Best practice: always remove listeners in onUnmounted to prevent memory leaks.
onMounted and onUnmounted are Composition API lifecycle hooks for setup and cleanup. onMounted: called after component is inserted into DOM, perfect for: (1) DOM manipulation (element dimensions, focus management), (2) Initialize third-party libraries (charts, maps), (3) Add event listeners, (4) Fetch initial data, (5) Start timers/intervals. Example: onMounted(() => { inputRef.value.focus(); api.fetchData(); }). Runs once per component instance. onUnmounted: called before component is removed from DOM, critical for cleanup to prevent memory leaks: (1) Remove event listeners, (2) Clear timers/intervals, (3) Cancel pending requests, (4) Destroy third-party instances. Example: onUnmounted(() => { window.removeEventListener('resize', handler); clearInterval(intervalId); controller.abort(); }). Execution order: setup() → onMounted() → ... → onUnmounted(). Comparison to Options API: onMounted ≈ mounted(), onUnmounted ≈ beforeUnmount(). Best practice: every setup action in onMounted should have corresponding cleanup in onUnmounted. Use VueUse composables (onKeyStroke, useEventListener) for automatic cleanup.
Always remove event listeners in onUnmounted hook to prevent memory leaks. Leak pattern: onMounted(() => { window.addEventListener('resize', handleResize); }); // LEAK! No cleanup. Fixed pattern: const handleResize = () => { /* logic */ }; onMounted(() => { window.addEventListener('resize', handleResize); }); onUnmounted(() => { window.removeEventListener('resize', handleResize); }); // Cleanup. Important: use same function reference in add/remove (avoid anonymous functions). Best solution: VueUse's useEventListener: import { useEventListener } from '@vueuse/core'; useEventListener(window, 'resize', handleResize); // Auto cleanup! Other leak sources: (1) Intervals: const id = setInterval(() => {}, 1000); onUnmounted(() => clearInterval(id)); (2) Timeouts: const id = setTimeout(() => {}, 1000); onUnmounted(() => clearTimeout(id)); (3) Third-party libraries: onUnmounted(() => { chart.destroy(); map.remove(); }). Detect leaks: Chrome DevTools → Memory → Take heap snapshot before/after component mount/unmount, check for detached DOM nodes. Production best practice: use VueUse composables which handle cleanup automatically.
Use . Parent usage:
Use promise-based dialog pattern for reusable, async-friendly confirmations. Pattern: dialog function returns Promise that resolves with user choice. Implementation with vue3-promise-dialog: Create ConfirmDialog.vue with props (message, confirmText, cancelText) and two buttons that call close(true) or close(false). Register: app.use(PromiseDialog). Usage: const confirmed = await openDialog(ConfirmDialog, { message: 'Delete item?' }); if (confirmed) { deleteItem(); }. Benefits: (1) Call from any JS/TS file (not just components), (2) Async/await syntax (cleaner than callbacks), (3) TypeScript type safety for props and return values, (4) Single dialog instance reused across app. Alternative: create global dialog service with provide/inject, but promise pattern is more intuitive. Renderless design: separate logic (promise handling) from presentation (styling) for maximum reusability across different UI frameworks.
Use vue3-promise-dialog library for type-safe promise-based dialogs. Setup: npm install vue3-promise-dialog, then in main.ts: import { PromiseDialog } from 'vue3-promise-dialog'; app.use(PromiseDialog);. Create dialog component with TypeScript props interface: interface Props { title: string; message: string; }; const props = defineProps
Custom dialogs provide superior UX and functionality over native alert(). Key benefits: (1) Non-blocking: native alert() blocks JavaScript execution and UI rendering, custom dialogs use async/await without blocking, (2) Customizable: full control over styling, branding, animations, layout vs fixed browser chrome, (3) Accessibility: add ARIA attributes, focus management, keyboard navigation, screen reader support, (4) Rich content: include forms, images, complex HTML vs plain text only, (5) Consistent UX: same look across all browsers/OS vs native browser styling, (6) Better mobile experience: responsive design, touch-friendly buttons vs tiny native buttons, (7) Testable: easy to unit test custom components vs difficulty testing native alerts. Technical advantages: promise-based API (cleaner code), can programmatically close, support multiple simultaneous dialogs, integrate with state management (Pinia/Vuex). Native alert() only acceptable for quick debugging, never production code.
Implement dialog queue system to prevent multiple overlapping modals. Strategy 1: Single dialog instance with queue. Use reactive queue: const dialogQueue = ref<DialogConfig[]>([]); const currentDialog = computed(() => dialogQueue.value[0]); function openDialog(config) { dialogQueue.value.push(config); } function closeDialog() { dialogQueue.value.shift(); }. Only render when currentDialog exists. Strategy 2: Disable trigger buttons when dialog open. Track open state: const isDialogOpen = ref(false);
Accessible confirmation dialogs must support standard keyboard shortcuts per WCAG 2.1 guidelines. Required shortcuts: (1) Escape key: close dialog (most critical), (2) Tab/Shift+Tab: cycle focus within dialog (focus trap), (3) Enter key: activate focused button (typically 'Confirm'), (4) Space key: activate focused button. Implementation with Vue 3 Composition API: onMounted(() => { window.addEventListener('keydown', handleKeydown); }); onUnmounted(() => { window.removeEventListener('keydown', handleKeydown); }); const handleKeydown = (e: KeyboardEvent) => { if (e.key === 'Escape') close(false); if (e.key === 'Enter' && focusedElement === confirmButton) confirm(); }. Best practices: (1) Focus 'Cancel' button by default (safer, prevents accidental confirmation), (2) Highlight focused button visibly (outline, background color), (3) Arrow keys for button navigation (left/right). Avoid: requiring mouse for any action, keyboard traps user cannot escape, no visible focus indicators.
Props and slots serve different purposes in modal components. Props: pass configuration data (title, size, closable, variant), typically simple types (string, boolean, number). Example:
Teleport renders component content outside current DOM hierarchy, essential for modals to avoid CSS stacking issues. Basic implementation:
Z-index stacking context determines layering order of positioned elements. New stacking contexts created by: position (absolute/relative/fixed) + z-index, transform, opacity < 1, filter, will-change. Problem: z-index only works within same stacking context, child with z-index: 9999 cannot appear above parent's sibling with z-index: 2. Modal solution: use Teleport to move modal outside parent stacking contexts:
Focus trap keeps keyboard navigation within modal, essential for accessibility. Solution: use focus-trap library with VueUse's useFocusTrap composable. Installation: npm install focus-trap. Implementation: import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'; const modalRef = ref(); const { activate, deactivate } = useFocusTrap(modalRef); onMounted(() => activate()); onUnmounted(() => deactivate());. How it works: (1) Finds all focusable elements (buttons, inputs, links) within modal, (2) Tab from last element returns to first, (3) Shift+Tab from first returns to last, (4) Escape key deactivates trap. Manual implementation: const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const focusable = modalRef.value.querySelectorAll(focusableSelector); document.addEventListener('keydown', (e) => { if (e.key === 'Tab') { /* cycle through focusable */ } });. Best practices: (1) Focus first interactive element on open (close button or primary action), (2) Return focus to trigger element on close, (3) Use aria-modal="true" to tell screen readers only modal is accessible.
Accessible modals require specific ARIA attributes per WCAG 2.1 guidelines. Required attributes: (1) role="dialog" or role="alertdialog" on modal container (alertdialog for critical confirmations), (2) aria-modal="true" tells screen readers only modal content is accessible (hides background), (3) aria-labelledby="title-id" references modal title element ID, (4) aria-describedby="desc-id" references description/content element ID. Example:
Delete User
This action cannot be undone.
Backdrop click should close modal but requires careful event handling to prevent bugs. Pattern: listen for click on overlay (backdrop), not modal content. Implementation: <div class="overlay" @click="handleOverlayClick"><div class="modal" @click.stop>Content
Use Vue 3 Composition API with onMounted/onUnmounted for proper event listener cleanup. Pattern: onMounted(() => { window.addEventListener('keydown', handleKeydown); }); onUnmounted(() => { window.removeEventListener('keydown', handleKeydown); }); const handleKeydown = (e: KeyboardEvent) => { if (e.key === 'Escape') close(); }. Key modifiers in templates: @keydown.enter="submit", @keydown.esc="close", @keydown.ctrl.s.prevent="save" (Ctrl+S). System modifiers: .ctrl, .alt, .shift, .meta (Cmd on Mac). Exact modifier: @keydown.ctrl.exact prevents trigger when other keys pressed. VueUse composables: import { onKeyStroke } from '@vueuse/core'; onKeyStroke('Escape', () => close()); automatically handles cleanup. Global vs component listeners: window.addEventListener for app-wide shortcuts, @keydown on specific elements for component-scoped. Performance: use event.key (modern) not event.keyCode (deprecated). Prevent default: e.preventDefault() stops browser default behavior. Best practice: always remove listeners in onUnmounted to prevent memory leaks.
onMounted and onUnmounted are Composition API lifecycle hooks for setup and cleanup. onMounted: called after component is inserted into DOM, perfect for: (1) DOM manipulation (element dimensions, focus management), (2) Initialize third-party libraries (charts, maps), (3) Add event listeners, (4) Fetch initial data, (5) Start timers/intervals. Example: onMounted(() => { inputRef.value.focus(); api.fetchData(); }). Runs once per component instance. onUnmounted: called before component is removed from DOM, critical for cleanup to prevent memory leaks: (1) Remove event listeners, (2) Clear timers/intervals, (3) Cancel pending requests, (4) Destroy third-party instances. Example: onUnmounted(() => { window.removeEventListener('resize', handler); clearInterval(intervalId); controller.abort(); }). Execution order: setup() → onMounted() → ... → onUnmounted(). Comparison to Options API: onMounted ≈ mounted(), onUnmounted ≈ beforeUnmount(). Best practice: every setup action in onMounted should have corresponding cleanup in onUnmounted. Use VueUse composables (onKeyStroke, useEventListener) for automatic cleanup.
Always remove event listeners in onUnmounted hook to prevent memory leaks. Leak pattern: onMounted(() => { window.addEventListener('resize', handleResize); }); // LEAK! No cleanup. Fixed pattern: const handleResize = () => { /* logic */ }; onMounted(() => { window.addEventListener('resize', handleResize); }); onUnmounted(() => { window.removeEventListener('resize', handleResize); }); // Cleanup. Important: use same function reference in add/remove (avoid anonymous functions). Best solution: VueUse's useEventListener: import { useEventListener } from '@vueuse/core'; useEventListener(window, 'resize', handleResize); // Auto cleanup! Other leak sources: (1) Intervals: const id = setInterval(() => {}, 1000); onUnmounted(() => clearInterval(id)); (2) Timeouts: const id = setTimeout(() => {}, 1000); onUnmounted(() => clearTimeout(id)); (3) Third-party libraries: onUnmounted(() => { chart.destroy(); map.remove(); }). Detect leaks: Chrome DevTools → Memory → Take heap snapshot before/after component mount/unmount, check for detached DOM nodes. Production best practice: use VueUse composables which handle cleanup automatically.
Use