Accessibility

Focus Models

Choose the right focus behavior for anchored and floating UI.

Focus behavior is one of the easiest places for floating UI to feel either polished or frustrating. VFloat gives you several focus-related tools, but they make more sense when you think in focus models instead of individual options.

There are two focus questions you will run into again and again:

  • Where should DOM focus live?
  • What should happen when focus moves?

Focus stays on the trigger

Sometimes the trigger should keep focus while the floating surface behaves like a lightweight companion. This is common for simple tooltips and combobox-like inputs with active descendants.

  • useFocus detects when the trigger receives :focus-visible to open the surface, and closes when focus leaves the family.
  • Focus never leaves the input or trigger.

A minimal focus trigger takes one shared node:

<script setup lang="ts">
import { ref } from "vue";
import { useFloatingNode, useFocus } from "v-float";

const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const node = useFloatingNode({ anchorEl, floatingEl });

useFocus(node);
</script>

useFocus(node) opens and closes the same node every other interaction composable reads, so it composes with hover or click without extra state. Nested surfaces are recognized automatically as inside the family via node.contains().

Focus moves into the surface

Sometimes the floating content should become the focus destination. This is common for menus, action lists, dialogs, and modal content.

  • useFocusTrap handles the entire surface focus lifecycle: routing initial focus inside on open, trapping or wrapping Tab navigation, isolating outside content with inert, and returning focus to the trigger on close.

useFocus() and focus-visible behavior

useFocus is the composable that opens and closes a surface from anchor focus and blur.

One important detail is that it is keyboard-first by default. It respects focus-visible behavior (requireFocusVisible: true), which means pointer-triggered focus does not automatically behave the same way as keyboard-triggered focus. Blur handling is deferred a tick and reads activeElement, so Shadow DOM and programmatic focus moves close reliably.

useFocusTrap(): surface focus orchestration

useFocusTrap is for surfaces that take focus or must contain focus while open.

A modal surface takes the same shared node plus one option:

<script setup lang="ts">
import { ref } from "vue";
import { useFloatingNode, useFocusTrap } from "v-float";

const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const node = useFloatingNode({ anchorEl, floatingEl });

useFocusTrap(node, { modal: true });
</script>

Defaults are modal-first: modal: true, returnFocus: true, and outside elements are isolated with inert while open.

  • Modals and dialogs. Traps focus inside, wraps Tab / Shift+Tab, and applies inert to the outside page.
  • Non-modal surfaces. Focus is managed without trapping. Opt in to closeOnFocusOut when leaving should dismiss.
  • Return focus. Safely restores focus to the trigger on close without viewport jumps.
  • Nested families. Family awareness is built into the node via node.contains(). Closing a parent never cascades on its own; call node.traverse((descendant) => descendant.open.value = false, { order: "bottom-up" }) when teardown must close the entire family cascade.

Where to go next

Released under the MIT License. Copyright © 2026