Advanced

Safe Polygons

Avoid common safe polygon edge cases when moving between surfaces.

safePolygon can make hover-based floating UI feel much better, but it can also make a surface feel oddly sticky if you apply it without thinking about the pointer path.

What problem it solves

When there is a visible gap between the trigger and the floating element, a plain pointerleave close is often too eager. The user leaves the trigger while moving toward the floating content, and the UI closes before they get there.

safePolygon keeps the surface open while the pointer travels through a protected corridor between the anchor and the floating element.

It is an option of useHover, which opens and closes with reason hover and never closes a surface pinned by another reason.

Enabling the corridor with defaults takes one option:

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

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

const node = useFloatingNode({ anchorEl, floatingEl });

useHover(node, { safePolygon: true });
</script>

Passing true enables the corridor with defaults (buffer: 0.5, requireIntent: true); pass an object to tune them. The option defaults to false.

The main tradeoff

The more forgiving the corridor becomes, the less tightly it matches the visible UI. That can create a strange feeling where the pointer appears to have left the UI, but the surface stays open because the safe area is larger than it looks.

Background hover flicker in mega-menus

In mega-menus, multi-column navigation bars, and dense toolbars, moving diagonally from a trigger to a floating panel means crossing intermediate links and buttons. While safePolygon keeps the active floating panel open, moving the pointer across background DOM elements still fires their native hover and pointer events. Sibling navigation items flash their hover states as the cursor glides past.

Pass blockPointerEvents: true to prevent underlying elements from receiving pointer events while the cursor stays within the safe corridor. By default, this applies to document.body. To keep the rest of the page interactive and restrict shielding to a specific container, pass getScope:

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

const navContainerEl = useTemplateRef<HTMLElement>("navContainer");
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);

const node = useFloatingNode({ anchorEl, floatingEl });

useHover(node, {
  safePolygon: {
    blockPointerEvents: true,
    getScope: () => navContainerEl.value,
  },
});
</script>

Underlying elements within the scoped container stop firing pointer events during corridor transit, eliminating flicker without modifying global CSS. Reference-counted hold tracking ensures that nested menus and overlapping corridors never strand pointer-events disabled. Pointer events restore immediately once the pointer enters the floating panel, returns to the anchor, or leaves the corridor.

Abrupt closes on wide layouts

By default, safePolygon checks whether the user is still moving toward the floating element (requireIntent: true). If the cursor slows down below 0.1 px/ms, a 40ms timer triggers closure.

In wide layouts or large mega-menus, users travel longer screen distances and often pause briefly mid-transit while reading or reorienting. If 40ms feels too abrupt, increase intentTimeout:

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

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

const node = useFloatingNode({ anchorEl, floatingEl });

useHover(node, {
  safePolygon: {
    intentTimeout: 100,
  },
});
</script>

A value between 80ms and 150ms gives users a forgiving pause window. Keep this bounded: excessively high values make menus feel unresponsive when users deliberately move away.

Opposite-side departures skip the corridor

safePolygon only activates when the pointer exits the anchor toward the floating panel. If the user moves away from the floating panel in the opposite direction (for example, leaving via the left edge of an anchor when the floating panel is positioned to the right), useHover detects the departure immediately and skips corridor creation. This avoids sticky or sluggish dismissals when the user is navigating away from the floating UI.

Debug the corridor if hover feels wrong

If the behavior feels surprising, pass safePolygon: { onPolygonChange } to inspect the polygon while debugging. Clearing the corridor (on close or re-enter) reports an empty polygon.

For nested menus, useHover uses intrinsic family awareness (node.contains()) so moving into a descendant surface's anchor or floating element does not prematurely close the parent.

Where to go next

Released under the MIT License. Copyright © 2026