Hierarchy
When you nest floating surfaces (like a submenu inside a dropdown or a select menu inside a modal), <Teleport to="body"> breaks their DOM ancestry.
In the DOM, the parent and child become flat siblings under <body>. Without coordination, clicking the child closes the parent, and pressing Escape dismisses the entire stack at once.
VFloat solves this by keeping parent-child links in the node itself.
Connecting parent and child
Every FloatingNode tracks its parent and child relationships. You link them in one of two ways:
1. Automatic injection (parent: "auto")
In multi-component setups, a child component automatically finds its nearest ancestor floating node using Vue provide and inject:
<!-- SubMenu.vue -->
<script setup lang="ts">
import { ref } from "vue";
import { useFloatingNode, usePosition } from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
// Automatically links to the parent menu node via Vue DI
const node = useFloatingNode({
anchorEl,
floatingEl,
parent: "auto",
});
usePosition(node, { placement: "right-start" });
</script>
2. Explicit reference (parent: parentNode)
In a single component, pass the parent node directly:
const root = useFloatingNode({ anchorEl: rootBtn, floatingEl: rootMenu });
const sub = useFloatingNode({ anchorEl: subBtn, floatingEl: subMenu, parent: root });
If parent is omitted, the node remains completely standalone. It will never adopt or be adopted by other surfaces accidentally.
Complete nested menu setup
Here is a full working setup coordinating a root menu with a child submenu:
<script setup lang="ts">
import { ref } from "vue";
import {
useClick,
useEscapeKey,
useFloatingNode,
useHover,
useOutsideClick,
usePosition,
} from "v-float";
const rootAnchor = ref<HTMLElement | null>(null);
const rootFloating = ref<HTMLElement | null>(null);
const subAnchor = ref<HTMLElement | null>(null);
const subFloating = ref<HTMLElement | null>(null);
// 1. Establish node hierarchy
const root = useFloatingNode({ anchorEl: rootAnchor, floatingEl: rootFloating });
const sub = useFloatingNode({ anchorEl: subAnchor, floatingEl: subFloating, parent: root });
// 2. Position both surfaces
usePosition(root, { placement: "bottom-start" });
usePosition(sub, { placement: "right-start" });
// 3. Root menu interactions
useClick(root);
useOutsideClick(root);
useEscapeKey(root);
// 4. Submenu interactions with hover intent and safe corridor
useHover(sub, {
delay: { open: 100, close: 200 },
safePolygon: true,
});
useOutsideClick(sub);
useEscapeKey(sub);
</script>
<template>
<button ref="rootAnchor" type="button">Options</button>
<Teleport to="body">
<div v-if="root.open.value" ref="rootFloating" class="menu">
<button type="button">New document</button>
<button ref="subAnchor" type="button">Share ›</button>
<button type="button">Delete</button>
</div>
<div v-if="sub.open.value" ref="subFloating" class="submenu">
<button type="button">Copy link</button>
<button type="button">Email invite</button>
</div>
</Teleport>
</template>
What hierarchy coordinates
Outside click protection
When useOutsideClick checks whether a click happened outside a parent menu, it calls node.contains(event.target).
Instead of checking only its own DOM element, node.contains recursively checks open children. Clicking inside a teleported child submenu counts as inside the family, so the parent stays open.
Leaf-first Escape dismissal
When the user presses Escape, useEscapeKey checks whether the current node has any open children:
- If open children exist, the node ignores the keypress.
- Only the innermost leaf node closes.
Pressing Escape a second time closes the next parent up the chain, guaranteeing predictable step-by-step dismissal.
Cascading programmatic teardown
When the root menu closes (for example, on outside click or menuitem selection), you can tear down all open descendants using node.traverse:
function closeAll() {
root.traverse(
(descendant) => {
descendant.open.value = false;
},
{ order: "bottom-up" },
);
root.open.value = false;
}
Visiting descendants bottom-up ensures child cleanup hooks and state listeners run before the root unmounts.
Focus trap family coordination
When a modal dialog opens a select picker or date dropdown, useFocusTrap recognizes that focus has moved into a child floating surface via node.contains(). The parent modal does not steal focus back or trap the user inside the dialog boundary.
Automatic cleanup
When a child component unmounts, Vue's scope disposal automatically removes the child from the parent's children set and clears the parent reference.
Where to go next
- Keyboard Navigation: Add roving focus and arrow key navigation to menus and submenus.
- Hover & Dismissal: Learn how safe polygons protect diagonal mouse paths into submenus.
useFloatingNodeAPI: Full options and parenting details.