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

NameTypeDefaultNotes
enabledMaybeRefOrGetter<boolean>trueReactive toggle. Activates focus management while node.open is true.
modalMaybeRefOrGetter<boolean>trueWhen true, isolates outside DOM elements with inert and strictly traps Tab navigation inside.
initialFocusElement, ref, function, or falseFirst tabbable elementSpecifies element to receive focus upon opening. false prevents initial focus.
returnFocusboolean, Element, or reftrueRestores focus to the trigger or target element when the trap deactivates.
closeOnFocusOutMaybeRefOrGetter<boolean>falseWhen modal: false, closes node when focus leaves the floating family.
preventScrollMaybeRefOrGetter<boolean>truePrevents browser viewport scrolling when shifting focus.
ignoreFocusOut(target: EventTarget | null) => booleanundefinedCustom predicate to ignore focus loss to specific target elements.
onError(error: unknown) => voidundefinedOptional error handler callback if trap activation fails.

Returns

NameTypeNotes
isActiveComputedRef<boolean>Reactive status indicating whether focus trapping is currently active.
activate() => voidManually activates focus management.
deactivate() => voidManually deactivates focus management and restores focus.

Details

  • Modal Dialogs (modal: true): Pressing Tab on the last element wraps back to the first. Background DOM elements outside the floating family are marked inert to 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: true gracefully 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 initialFocus is 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

Released under the MIT License. Copyright © 2026