Focus Models
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.
useFocusdetects when the trigger receives:focus-visibleto 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.
useFocusTraphandles the entire surface focus lifecycle: routing initial focus inside on open, trapping or wrapping Tab navigation, isolating outside content withinert, 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
inertto the outside page. - Non-modal surfaces. Focus is managed without trapping. Opt in to
closeOnFocusOutwhen 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; callnode.traverse((descendant) => descendant.open.value = false, { order: "bottom-up" })when teardown must close the entire family cascade.
Where to go next
- Read Keyboard Navigation for list-level focus decisions.
- Read Build Dialogs and Modals for modal focus flows.
- Read List Navigation Gotchas for the most common focus mistakes in floating lists.