Dialogs & Modals
Dialogs and modals are floating surfaces that carry stronger behavioral expectations than tooltips, popovers, or menus. Once a surface behaves like a dialog, users expect focus to be trapped intentionally, outside content to become inert, and dismissal to be explicit.
This guide shows a practical VFloat setup for dialog-like surfaces and explains when a popover should become a dialog.
What makes a dialog different
A dialog usually needs:
- Click or programmatic activation
- Focus containment and trapping while open
- Outside page isolation (
inert) so screen readers and Tab keys cannot reach background content - Safe focus return to the trigger element on close
- Explicit semantics such as
role="dialog"andaria-modal="true" - Escape key and backdrop dismissal
In VFloat, the core dialog stack pairs useFloatingNode with useFocusTrap, useEscapeKey, and useOutsideClick.
The complete example
Here is a full working modal dialog:
<script setup lang="ts">
import { ref } from "vue";
import { useClick, useEscapeKey, useFloatingNode, useFocusTrap, useOutsideClick } from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
const node = useFloatingNode({ anchorEl, floatingEl });
useClick(node);
useEscapeKey(node);
useOutsideClick(node);
useFocusTrap(node, {
modal: true,
});
</script>
<template>
<button ref="anchorEl" type="button">Edit profile</button>
<Teleport to="body">
<div v-if="node.open.value" class="dialog-backdrop">
<div
ref="floatingEl"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabindex="-1"
class="dialog-panel"
>
<h2 id="dialog-title">Edit profile</h2>
<p>Update your display name and email address.</p>
<form @submit.prevent="node.open.value = false">
<input type="text" placeholder="Your name" />
<button type="submit">Save</button>
<button type="button" @click="node.open.value = false">Cancel</button>
</form>
</div>
</div>
</Teleport>
</template>
Focus trapping and page isolation
useFocusTrap(node, {
modal: true,
});
useFocusTrap orchestrates the entire focus lifecycle for the dialog:
- Initial focus. Automatically focuses the first tabbable element inside
floatingElwhen the dialog opens. - Tab cycling. Traps Tab and Shift+Tab inside the dialog container so focus cannot escape into the browser chrome.
- Page isolation. Applies
inertto outside page elements so assistive technology cannot interact with background content while the dialog is visible. - Return focus. Safely restores focus back to the trigger button (
anchorEl) when the dialog closes.
Anchored dialogs vs centered modals
Modal dialogs often center in the viewport using CSS Flexbox or Grid over a full-screen backdrop. In that case, you do not need usePosition because the dialog does not anchor to a specific element.
When building an anchored dialog (such as a callout dialog, rich popover dialog, or contextual confirmation bubble next to a button), add usePosition:
usePosition(node, {
placement: "bottom-start",
middlewares: {
offset: 8,
flip: true,
shift: { padding: 8 },
},
});
usePosition will automatically apply the calculated positioning coordinates to floatingEl. VFloat allows you to mix and match focus trapping with or without JavaScript positioning based on your UI needs.
When a popover should become a dialog
If your surface captures user attention for a critical flow, contains multiple interactive form fields, or requires focus containment so keyboard navigation does not wander off the surface, you are building a dialog, not a popover.
Where to go next
- Read Controlled vs Uncontrolled to decide whether the component or an outside router/store should own open state.
- Read Focus Models for the deeper mental model behind focus management.