useFloatingNode
useFloatingNode creates a unified composite floating node. It owns element refs and open state, providing a stable identity, spatial containment checks, and reactive hub for positioning and interaction composables.
useFloatingNode acts as both a standalone surface ($N = 0$) and a hierarchical tree node ($N > 0$). It operates as a standalone surface by default, and supports hierarchical composite trees via explicit parent references or opt-in Dependency Injection (parent: "auto").
Type
function useFloatingNode(options: UseFloatingNodeOptions): FloatingNode;
interface UseFloatingNodeOptions {
anchorEl: Ref<AnchorElement>;
floatingEl: Ref<FloatingElement>;
arrowEl?: Ref<HTMLElement | null>;
open?: Ref<boolean>;
parent?: FloatingNode | "auto" | null;
provide?: boolean;
}
type AnchorElement = HTMLElement | VirtualElement | null;
type FloatingElement = HTMLElement | null;
type FloatingNodeId = symbol;
interface FloatingNodeElements {
anchorEl: Ref<AnchorElement>;
floatingEl: Ref<FloatingElement>;
arrowEl: Ref<HTMLElement | null>;
}
interface FloatingNode {
id: FloatingNodeId;
refs: FloatingNodeElements;
open: Ref<boolean>;
parent: Readonly<ShallowRef<FloatingNode | null>>;
children: Readonly<ShallowRef<ReadonlySet<FloatingNode>>>;
appendChild: (child: FloatingNode) => () => void;
removeChild: (child: FloatingNode) => void;
contains: (target: EventTarget | null) => boolean;
traverse: (
visitor: (node: FloatingNode, depth: number) => TraverseAction,
options?: TraverseOptions,
) => boolean;
}
type TraverseAction = void | "skip" | "stop";
interface TraverseOptions {
order?: "top-down" | "bottom-up";
}
Options
| Name | Type | Default | Notes |
|---|---|---|---|
anchorEl | Ref<AnchorElement> | Required | Reference element or virtual element. |
floatingEl | Ref<FloatingElement> | Required | Floating content element. |
arrowEl | Ref<HTMLElement | null> | ref(null) | Optional arrow element ref. Automatically created when omitted. |
open | Ref<boolean> | ref(false) | Reactive mutable open state ref. When omitted, an internal ref(false) is created. Pass open: ref(true) to initialize open. |
parent | FloatingNode | "auto" | null | undefined | Parent node reference. Omitted/undefined (default) is standalone; "auto" links via DI; FloatingNode links explicitly; null isolates. |
provide | boolean | true | Whether to provide this node to descendant components via Vue's Dependency Injection. |
Returns
| Name | Type | Notes |
|---|---|---|
id | FloatingNodeId | Stable symbol identifying the node in trees. |
refs | FloatingNodeElements | Shared anchorEl, floatingEl, and arrowEl refs. |
open | Ref<boolean> | Reactive mutable open state ref. Mutate directly (node.open.value = true / false). |
parent | Readonly<ShallowRef<FloatingNode | null>> | Intrinsic parent node in the hierarchy. null for root or standalone nodes. |
children | Readonly<ShallowRef<ReadonlySet<FloatingNode>>> | Immediate child nodes registered under this node. |
appendChild | (child: FloatingNode) => () => void | Atomically links a child node under this parent. Returns a teardown function. |
removeChild | (child: FloatingNode) => void | Unlinks a child node and clears the child's parent reference. |
contains | (target: EventTarget | null) => boolean | Checks whether target is contained in this node or any open descendant. |
traverse | (visitor, options?) => boolean | Recursively traverses this node and its descendants in depth-first order. |
Details
Pure Vue Open State
node.open is a standard Vue Ref<boolean>. You can drive open state directly in script or templates:
// Mutate state directly
node.open.value = true;
node.open.value = false;
When you pass an external ref to useFloatingNode({ anchorEl, floatingEl, open: isOpen }), node.open aliases isOpen directly:
const isOpen = ref(false);
const node = useFloatingNode({ anchorEl, floatingEl, open: isOpen });
// Mutating either updates both
isOpen.value = true;
console.log(node.open.value); // true
When you omit open, useFloatingNode creates an internal ref(false) returned as node.open. If you want the node to start open, pass open: ref(true).
Hierarchy & Parenting Resolution
useFloatingNode implements a three-tier parenting strategy:
- Standalone (Default / Omitted /
parent: undefined/parent: null): By default, a floating node is completely independent (parent.value === null). It does not inject an ancestor parent, ensuring top-level surfaces (such as tooltips inApp.vueor layout components) never accidentally adopt or get adopted by child floating surfaces. - Opt-In DI (
parent: "auto"): Passingparent: "auto"explicitly discovers and attaches to the nearest ancestorFloatingNodeprovided via Vue's Dependency Injection (provide/inject). This allows multi-component cascading submenus to link automatically without manual prop drilling. - Explicit Parent (
parent: rootNode): Passing an explicitFloatingNodelinks directly to that parent, bypassing DI. This is ideal for flat single-component<script setup>scripts or explicit prop forwarding.
Spatial Containment (node.contains)
node.contains(target) determines if an event target belongs to the floating surface or any of its open descendants. Interaction composables such as useOutsideClick, useEscapeKey, and useHover rely on node.contains to prevent premature dismissals when interacting with nested submenus or child panels.
Examples
Multi-Component Submenu (Opt-In DI)
In multi-component architectures, submenus opt into parent discovery and registration via parent: "auto" without manual prop drilling:
<!-- RootMenu.vue -->
<script setup lang="ts">
import { ref } from "vue";
import { useEscapeKey, useFloatingNode, useOutsideClick, usePosition } from "v-float";
import SubMenu from "./SubMenu.vue";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
// Automatically provides this node to child components (provide defaults to true)
const node = useFloatingNode({ anchorEl, floatingEl });
usePosition(node);
useOutsideClick(node);
useEscapeKey(node);
</script>
<template>
<button ref="anchorEl" @click="node.open.value = !node.open.value">Menu</button>
<div v-if="node.open.value" ref="floatingEl" class="menu">
<SubMenu />
</div>
</template>
<!-- SubMenu.vue -->
<script setup lang="ts">
import { ref } from "vue";
import { useEscapeKey, useFloatingNode, useOutsideClick, usePosition } from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
// Opt into DI parenting to link with ancestor RootMenu node
const subNode = useFloatingNode({ anchorEl, floatingEl, parent: "auto" });
usePosition(subNode, { placement: "right-start" });
useOutsideClick(subNode);
useEscapeKey(subNode);
</script>
<template>
<button ref="anchorEl" @click="subNode.open.value = !subNode.open.value">Submenu</button>
<div v-if="subNode.open.value" ref="floatingEl" class="submenu">
<p>Submenu items</p>
</div>
</template>
Flat Script (Explicit Parent)
In flat single-component scripts where submenus are defined in the same <script setup>, pass the parent node explicitly:
<script setup lang="ts">
import { ref } from "vue";
import { useEscapeKey, useFloatingNode, useOutsideClick, usePosition } from "v-float";
const rootAnchorEl = ref<HTMLElement | null>(null);
const rootFloatingEl = ref<HTMLElement | null>(null);
const subAnchorEl = ref<HTMLElement | null>(null);
const subFloatingEl = ref<HTMLElement | null>(null);
const root = useFloatingNode({ anchorEl: rootAnchorEl, floatingEl: rootFloatingEl });
const sub = useFloatingNode({ anchorEl: subAnchorEl, floatingEl: subFloatingEl, parent: root });
usePosition(root);
usePosition(sub, { placement: "right-start" });
useOutsideClick(root);
useEscapeKey(root);
useOutsideClick(sub);
useEscapeKey(sub);
</script>
See Also
usePosition- Add reactive coordinate calculationsuseOutsideClick- Coordinate outside clicks across node hierarchiesuseEscapeKey- Coordinate leaf-first Escape key dismissals across node hierarchiesuseClick- Toggle open state on click or tap- Floating Node - Conceptual guide to floating nodes