Skip to content

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

ts
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

NameTypeDefaultNotes
anchorElRef<AnchorElement>RequiredReference element or virtual element.
floatingElRef<FloatingElement>RequiredFloating content element.
arrowElRef<HTMLElement | null>ref(null)Optional arrow element ref. Automatically created when omitted.
openRef<boolean>undefinedControlled mutable open ref. When supplied, defaultOpen is ignored.
defaultOpenbooleanfalseInitial open state when open is omitted.
parentMaybeRefOrGetter<FloatingNode | null | undefined>undefinedParent node reference. Omitted/undefined uses DI; null forces standalone; FloatingNode/ref links explicitly.

Returns

NameTypeNotes
idFloatingNodeIdStable symbol identifying the node in trees.
refsFloatingNodeElementsShared anchorEl, floatingEl, and arrowEl refs.
openRef<boolean>Reactive mutable open state ref. Mutate directly (node.open.value = true / false).
parentReadonly<ShallowRef<FloatingNode | null>>Intrinsic parent node in the hierarchy. null for root or standalone nodes.
childrenReadonly<ShallowRef<ReadonlySet<FloatingNode>>>Immediate child nodes registered under this node.
appendChild(child: FloatingNode) => () => voidAtomically links a child node under this parent. Returns a teardown function.
removeChild(child: FloatingNode) => voidUnlinks a child node and clears the child's parent reference.
contains(target: EventTarget | null) => booleanChecks whether target is contained in this node or any open descendant.
traverse(visitor, options?) => booleanRecursively 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:

ts
// 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:

ts
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(defaultOpen ?? false) returned as node.open.

Hierarchy & Parenting Resolution

useFloatingNode implements a three-tier parenting strategy:

  1. Implicit DI (Default / Omitted): When parent is omitted or undefined, useFloatingNode automatically injects the nearest ancestor FloatingNode from the Vue component hierarchy via provide / inject. In addition, every node automatically provides itself to descendant components.
  2. Explicit Standalone (parent: null): Passing parent: null explicitly opts out of Dependency Injection. The node will remain a standalone root with parent.value === null even when nested inside an ancestor component that provides a floating node.
  3. Explicit Parent (parent: rootNode | ref | getter): Passing an explicit FloatingNode (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:

vue
<!-- 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>
vue
<!-- 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:

vue
<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 calculations
  • useDismiss - Coordinate outside clicks and Escape key dismissals across node hierarchies
  • useClick - Toggle open state on click or tap
  • Floating Node - Conceptual guide to floating nodes