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 supports automatic hierarchy wiring across components via Vue's Dependency Injection (provide / inject), explicit parent linking for single-component scripts, and explicit standalone isolation.
Type
function useFloatingNode(options: UseFloatingNodeOptions): FloatingNode;
interface UseFloatingNodeOptions {
anchorEl: Ref<AnchorElement>;
floatingEl: Ref<FloatingElement>;
arrowEl?: Ref<HTMLElement | null>;
open?: Ref<boolean>;
defaultOpen?: boolean;
parent?: MaybeRefOrGetter<FloatingNode | null | undefined>;
}
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> | undefined | Controlled mutable open ref. When supplied, defaultOpen is ignored. |
defaultOpen | boolean | false | Initial open state when open is omitted. |
parent | MaybeRefOrGetter<FloatingNode | null | undefined> | undefined | Parent node reference. Omitted/undefined uses DI; null forces standalone; FloatingNode/ref links explicitly. |
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); // trueWhen you omit open, useFloatingNode creates an internal ref(defaultOpen ?? false) returned as node.open.
Hierarchy & Parenting Resolution
useFloatingNode implements a three-tier parenting strategy:
- Implicit DI (Default / Omitted): When
parentis omitted orundefined,useFloatingNodeautomatically injects the nearest ancestorFloatingNodefrom the Vue component hierarchy viaprovide/inject. In addition, every node automatically provides itself to descendant components. - Explicit Standalone (
parent: null): Passingparent: nullexplicitly opts out of Dependency Injection. The node will remain a standalone root withparent.value === nulleven when nested inside an ancestor component that provides a floating node. - Explicit Parent (
parent: rootNode | ref | getter): Passing an explicitFloatingNode(or reactive ref/getter) links 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 useDismiss and useHover rely on node.contains to prevent premature dismissals when interacting with nested submenus or child panels.
Examples
Multi-Component Submenu (Implicit DI)
In multi-component architectures, submenus automatically discover and register with parent nodes via Dependency Injection without passing props:
<!-- RootMenu.vue -->
<script setup lang="ts">
import { ref } from "vue";
import { useDismiss, useFloatingNode, 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
const node = useFloatingNode({ anchorEl, floatingEl });
usePosition(node);
useDismiss(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 { useDismiss, useFloatingNode, usePosition } from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
// Omitted parent defaults to DI: automatically injects RootMenu node
const subNode = useFloatingNode({ anchorEl, floatingEl });
usePosition(subNode, { placement: "right-start" });
useDismiss(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 { useDismiss, useFloatingNode, 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" });
useDismiss(root);
useDismiss(sub);
</script>See Also
usePosition- Add reactive coordinate calculationsuseDismiss- Coordinate outside clicks and Escape key dismissals across node hierarchiesuseClick- Toggle open state on click or tap- Floating Node - Conceptual guide to floating nodes