Interactions
useFocusTrap
Manages modal focus containment, background inert isolation, and return focus.
useFocusTrap confines keyboard focus inside a floating panel. It handles initial focus placement, focus restoration on close, Tab key cycling, and background inert attribute isolation for modal dialogs.
Type
function useFocusTrap(node: FloatingNode, options?: UseFocusTrapOptions): UseFocusTrapReturn;
interface UseFocusTrapOptions {
enabled?: MaybeRefOrGetter<boolean>;
modal?: MaybeRefOrGetter<boolean>;
initialFocus?: HTMLElement | Ref<HTMLElement | null> | (() => HTMLElement | null | false) | false;
returnFocus?: MaybeRefOrGetter<boolean | HTMLElement | Ref<HTMLElement | null>>;
closeOnFocusOut?: MaybeRefOrGetter<boolean>;
preventScroll?: MaybeRefOrGetter<boolean>;
ignoreFocusOut?: (target: EventTarget | null) => boolean;
onError?: (error: unknown) => void;
}
interface UseFocusTrapReturn {
isActive: ComputedRef<boolean>;
activate: () => void;
deactivate: () => void;
}
Options
| Name | Type | Default | Notes |
|---|---|---|---|
enabled | MaybeRefOrGetter<boolean> | true | Reactive toggle. Activates focus management while node.open is true. |
modal | MaybeRefOrGetter<boolean> | true | When true, isolates outside DOM elements with inert and strictly traps Tab navigation inside. |
initialFocus | Element, ref, function, or false | First tabbable element | Specifies element to receive focus upon opening. false prevents initial focus. |
returnFocus | boolean, Element, or ref | true | Restores focus to the trigger or target element when the trap deactivates. |
closeOnFocusOut | MaybeRefOrGetter<boolean> | false | When modal: false, closes node when focus leaves the floating family. |
preventScroll | MaybeRefOrGetter<boolean> | true | Prevents browser viewport scrolling when shifting focus. |
ignoreFocusOut | (target: EventTarget | null) => boolean | undefined | Custom predicate to ignore focus loss to specific target elements. |
onError | (error: unknown) => void | undefined | Optional error handler callback if trap activation fails. |
Returns
| Name | Type | Notes |
|---|---|---|
isActive | ComputedRef<boolean> | Reactive status indicating whether focus trapping is currently active. |
activate | () => void | Manually activates focus management. |
deactivate | () => void | Manually deactivates focus management and restores focus. |
Details
Modal Traps vs Non-Modal Overlays
- Modal Dialogs (
modal: true): Pressing Tab on the last element wraps back to the first. Background DOM elements outside the floating family are markedinertto prevent screen readers or pointer clicks from escaping. - Non-Modal Overlays (
modal: false): Focus is not trapped, so Tab follows the natural document flow.closeOnFocusOut: truegracefully dismisses the panel when focus moves away.
Multiple Independent Modals
Each strict modal registers on a per-document stack. The newest modal owns Tab wrapping and focus corrections, and background inert isolation is reference-counted: closing one modal never strips the isolation another still needs. Traps that share one FloatingNode tree (submenus, cascades) coordinate automatically through the tree instead of the stack.
Initial and Return Focus
- When
initialFocusis omitted, the trap automatically focuses the first tabbable child (falling back to the floating container). - When deactivated (e.g. on dialog close), focus smoothly returns to the trigger button stored in
node.refs.anchorEl.
Example
<script setup lang="ts">
import { ref } from "vue";
import { useEscapeKey, useFloatingNode, useFocusTrap, useOutsideClick, useRole } from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
const nameInput = ref<HTMLInputElement | null>(null);
const node = useFloatingNode({ anchorEl, floatingEl });
useFocusTrap(node, {
modal: true,
initialFocus: nameInput,
returnFocus: true,
});
useEscapeKey(node);
useOutsideClick(node);
useRole(node, { role: "dialog", modal: true });
</script>
<template>
<button ref="anchorEl" @click="node.open.value = true">Edit Profile</button>
<div v-if="node.open.value" class="dialog-backdrop">
<div ref="floatingEl" class="dialog-panel">
<h2>Edit Profile</h2>
<input ref="nameInput" placeholder="Full name" />
<input placeholder="Email address" />
<div class="actions">
<button @click="node.open.value = false">Save</button>
<button @click="node.open.value = false">Cancel</button>
</div>
</div>
</div>
</template>
<style>
.dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.dialog-panel {
background: white;
padding: 24px;
border-radius: 8px;
width: 320px;
}
</style>
See Also
useEscapeKey- Close on Escape key pressuseOutsideClick- Close on outside or backdrop clickuseRole- Applyrole="dialog"andaria-modal="true"useFloatingNode- Shared node lifecycle- Build Dialogs and Modals - Full modal implementation guide
- Focus Models - Comparison of modal vs roving focus patterns