Positioning

Virtual Anchors

Position floating content from pointer coordinates, synthetic rectangles, and dynamic selections.

Sometimes there is no real DOM element to serve as an anchor. You may need to position a panel at the mouse cursor, pin a context menu to right-click coordinates, or anchor a formatting toolbar to a highlighted text selection.

VFloat supports these scenarios through virtual anchors.

Mental model: geometry over DOM elements

VFloat only requires a getBoundingClientRect() implementation to compute coordinates. An anchor does not need to be an HTMLElement. Any object that satisfies the VirtualElement contract can serve as an anchor:

interface VirtualElement {
  getBoundingClientRect(): DOMRect;
  contextElement?: Element;
}

VFloat provides two practical approaches for virtual anchors:

  1. useClientPoint: Automated pointer tracking for cursor-driven floating surfaces.
  2. Manual VirtualElement: Explicit coordinate rectangles for text selections, canvas nodes, and external events.

Pointer-driven positioning with useClientPoint

useClientPoint listens to pointer events and dynamically updates a virtual anchor reflecting the cursor's client coordinates.

Follow the cursor

In "follow" mode, the floating surface continuously tracks cursor movement while open:

<script setup lang="ts">
import { ref } from "vue";
import { useClientPoint, useFloatingNode, useHover, usePosition } from "v-float";

const trackingAreaEl = ref<HTMLElement | null>(null);
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const node = useFloatingNode({ anchorEl, floatingEl });
usePosition(node, {
  placement: "right-start",
  middlewares: {
    offset: 12,
  },
});

useClientPoint(node, {
  trackingAreaEl,
  trackingMode: "follow",
});

useHover(node);
</script>

<template>
  <div ref="trackingAreaEl" class="interactive-canvas">
    Hover anywhere in this area to inspect coordinates.

    <div v-if="node.open.value" ref="floatingEl" class="cursor-tooltip">Inspection panel</div>
  </div>
</template>

In "follow" mode, each mouse move event updates the virtual anchor and triggers repositioning.

Keep the opening point static (Context menus)

For context menus and right-click inspectors, you want the menu to open at the pointer's location at click time and remain fixed there, even as the cursor moves toward menu items:

<script setup lang="ts">
import { ref } from "vue";
import {
  useClientPoint,
  useEscapeKey,
  useFloatingNode,
  useOutsideClick,
  usePosition,
} from "v-float";

const areaEl = ref<HTMLElement | null>(null);
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const node = useFloatingNode({ anchorEl, floatingEl });
usePosition(node, {
  placement: "bottom-start",
  middlewares: {
    flip: true,
    shift: { padding: 8 },
  },
});

useClientPoint(node, {
  trackingAreaEl: areaEl,
  trackingMode: "static",
});

useOutsideClick(node);
useEscapeKey(node);

function onContextMenu(e: MouseEvent) {
  e.preventDefault();
  node.open.value = true;
}
</script>

<template>
  <div ref="areaEl" class="context-zone" @contextmenu="onContextMenu">
    Right-click inside this container.

    <div v-if="node.open.value" ref="floatingEl" class="context-menu">
      <ul>
        <li>Inspect element</li>
        <li>Copy link</li>
        <li>Reload</li>
      </ul>
    </div>
  </div>
</template>

Static mode captures coordinates at the triggering interaction and holds them steady, preventing the menu from drifting across the screen as the user navigates menu items.

Controlled programmatic coordinates

You can pass reactive x and y refs directly to useClientPoint. When both resolve to valid numbers, useClientPoint enters controlled mode and pointer event tracking detaches:

const cursorX = ref<number | null>(null);
const cursorY = ref<number | null>(null);

useClientPoint(node, {
  x: cursorX,
  y: cursorY,
});

This is useful when positioning coordinates are supplied by external state, such as WebSockets or canvas interaction engines.

Manual virtual anchors

When positioning against browser selections or non-pointer geometry, you can construct a VirtualElement directly and pass it to useFloatingNode:

Text selection toolbar

A common virtual anchor pattern is a floating formatting toolbar anchored to the user's highlighted text:

<script setup lang="ts">
import { ref } from "vue";
import { type VirtualElement, useFloatingNode, usePosition } from "v-float";

const floatingEl = ref<HTMLElement | null>(null);
const anchorEl = ref<VirtualElement | null>(null);
const open = ref(false);

const node = useFloatingNode({ anchorEl, floatingEl, open });
usePosition(node, {
  placement: "top",
  middlewares: {
    offset: 8,
    shift: { padding: 8 },
  },
});

function onMouseUp() {
  const selection = window.getSelection();

  if (!selection || selection.isCollapsed) {
    open.value = false;
    anchorEl.value = null;
    return;
  }

  const range = selection.getRangeAt(0);

  anchorEl.value = {
    getBoundingClientRect() {
      return range.getBoundingClientRect();
    },
  };
  open.value = true;
}
</script>

<template>
  <div class="prose" @mouseup="onMouseUp">
    <p>Select any text in this paragraph to see the toolbar appear.</p>

    <div v-if="open" ref="floatingEl" class="toolbar">
      <button type="button">Bold</button>
      <button type="button">Italic</button>
      <button type="button">Link</button>
    </div>
  </div>
</template>

Whenever the user highlights text, range.getBoundingClientRect() supplies the selection box, and usePosition places the toolbar above the selected text.

Real-world gotchas and realities

The placeholder anchor ref

When using useClientPoint, you must still pass an initial anchorEl ref to useFloatingNode({ anchorEl, floatingEl }).

useClientPoint overwrites node.refs.anchorEl with its internal virtual element when tracking starts. The initial anchorEl is simply a placeholder required by useFloatingNode's signature until the composable runs.

trackingMode is not reactive after setup

trackingMode ("follow" vs "static") is read during composable setup. Pick the mode up-front based on the surface type:

  • Use "follow" for hover inspect panels, cursor tooltips, and crosshairs.
  • Use "static" for context menus, dropdowns opened at click points, and radial menus.

Virtual anchors carry geometry, not semantics

A virtual anchor provides coordinates, but it does not represent an actual HTML <button> or <input>. It does not dispatch DOM events, manage tab index, receive focus rings, or carry ARIA attributes.

When building virtual anchor surfaces, you must handle accessibility deliberately:

  1. Focus ownership: When opening a context menu at cursor coordinates, explicitly move focus into the menu panel or first menu item using nextTick() or watch(open).
  2. Keyboard activation: Provide keyboard alternatives for right-click context menus (such as the Shift+F10 key or a visible menu button).
  3. Restoration: When closing a virtual anchor surface, decide where keyboard focus should return (such as the containing canvas or card element).

Where to go next

Released under the MIT License. Copyright © 2026