Hover & Dismissal
Floating surfaces triggered by pointer hover or keyboard focus are among the most common sources of accessibility failures on the web.
When a tooltip vanishes before a user can move their mouse over it, or when an overlay blocks underlying content and cannot be dismissed without losing keyboard position, the interface fails users relying on screen magnifiers, keyboard navigation, or motor assistance.
WCAG 2.1 Success Criterion 1.4.13 ("Content on Hover or Focus") establishes three mandatory rules for all hover- and focus-triggered surfaces: Dismissable, Hoverable, and Persistent.
This guide explains how VFloat primitives satisfy these requirements.
The WCAG 2.1 SC 1.4.13 checklist
Before building any tooltip, hover card, or popover, ensure your surface satisfies the three core criteria defined in WCAG 2.1 SC 1.4.13:
- Dismissable: Users must be able to dismiss the floating surface without moving their pointer or keyboard focus.
- Hoverable: If pointer hover reveals the surface, the pointer must be able to move over the surface content without the surface disappearing.
- Persistent: The surface must remain visible until the hover or focus trigger is removed, the user explicitly dismisses it, or its content is no longer valid.
Hoverable: bridging the cursor gap with safePolygon
When you position a tooltip or hover card with margin or offset middleware (such as offset: 8), an empty gap separates the trigger button and the floating panel.
In standard DOM event handling, the moment the user moves the mouse across that gap, the trigger fires a pointerleave event, destroying the floating surface before the cursor ever reaches it. This makes it impossible for screen magnifier users to pan across the tooltip or for motor-impaired users to select text inside it.
useHover solves this problem through the safePolygon option.
Here is an accessible hover card that remains open while the cursor travels to it:
<script setup lang="ts">
import { ref } from "vue";
import {
useEscapeKey,
useFloatingNode,
useFocus,
useHover,
useOutsideClick,
usePosition,
} from "v-float";
const anchorEl = ref<HTMLElement | null>(null);
const floatingEl = ref<HTMLElement | null>(null);
const node = useFloatingNode({ anchorEl, floatingEl });
usePosition(node, {
placement: "bottom",
middlewares: { offset: 8 },
});
// Enable safe polygon corridor for pointer traversal
useHover(node, {
safePolygon: true,
restMs: 100,
});
useFocus(node);
useEscapeKey(node);
useOutsideClick(node);
</script>
<template>
<button ref="anchorEl" type="button" aria-describedby="user-card">@alex</button>
<div v-if="node.open.value" id="user-card" ref="floatingEl" class="card">
<p>Alex Morgan: Design Engineer</p>
<a href="/profile">View full profile</a>
</div>
</template>
How safe polygons work
When safePolygon: true is configured, VFloat calculates a dynamic geometric polygon that connects the cursor's exit coordinates on the trigger to the bounding corners of the floating element.
As long as the mouse pointer moves within this invisible triangular or trapezoidal corridor toward the floating surface, the close timer is paused. Once the cursor enters the floating element itself, the surface remains open normally.
Preventing visual noise with hover intent
Triggering popups instantly on casual mouse movements creates visual distraction for users with cognitive or attention challenges.
The restMs option (defaulting to 0ms) requires the pointer to pause briefly over the trigger before opening the surface:
useHover(node, {
safePolygon: true,
restMs: 150,
});
If the cursor merely travels across the button on its way to another part of the screen, the surface never opens.
Dismissable: closing without moving focus via useEscapeKey
WCAG 1.4.13 requires that if additional content obscures page content or input fields, the user must be able to dismiss it without moving their pointer or keyboard focus.
useEscapeKey attaches a document-level key listener that sets node.open.value = false when the user presses Escape.
Because keyboard focus remains untouched on the anchor button, the user can dismiss the obstructing tooltip or card and continue reading without losing their place, satisfying WCAG 2.1 SC 2.1.1 (Keyboard).
Leaf-first hierarchy coordination
When interfaces contain nested menus, stacked dialogs, or a tooltip opened inside a modal, pressing Escape must not dismiss the entire interface at once.
useEscapeKey coordinates through FloatingNode hierarchy to enforce leaf-first dismissal:
- Only the deepest, topmost active surface in the tree closes on the first Escape press.
- Sibling and parent surfaces remain open.
- Subsequent Escape presses close ancestor surfaces one level at a time.
International input method safety (IME)
When users type using Input Method Editors (IME) for languages such as Japanese, Chinese, or Korean, pressing Escape cancels the current phonetic composition rather than closing the window.
useEscapeKey inspects keyboard composition events and automatically suppresses dismissal while an IME session is actively composing characters.
Persistent: avoiding arbitrary auto-closing timers
A common anti-pattern in floating interfaces is adding a setTimeout that automatically dismisses a tooltip after three or four seconds.
Automatic timeouts violate WCAG 1.4.13. Users with low vision who rely on screen magnifiers require time to pan across enlarged text. Users with reading difficulties or cognitive impairments must be able to read at their own pace.
A floating surface must remain visible until:
- The user deliberately moves the pointer off both the trigger and the floating surface (and outside the safe corridor).
- The user moves keyboard focus away from the trigger.
- The user explicitly dismisses the surface via the Escape key.
Pointer dismissal outside the family via useOutsideClick
For click-triggered surfaces (popovers, menus, dialogs), users expect clicking anywhere outside the open surface to dismiss it.
useOutsideClick listens for pointer interactions outside the floating family.
Crucially, it is hierarchy-aware: when a modal dialog opens a date picker or select menu teleported to document.body, clicking the teleported date picker is recognized as an internal interaction via node.contains(). The parent dialog does not close prematurely.
Respecting user preferences for reduced motion
Many floating surfaces use CSS scale, zoom, or slide transitions.
For users with vestibular disorders, motion and parallax effects can cause dizziness, nausea, and physical disorientation (WCAG 2.1 SC 2.3.3: Animation from Interactions).
When styling your floating surfaces, always wrap motion transitions in the standard CSS @media (prefers-reduced-motion: reduce) media query:
.floating-panel {
transition:
opacity 150ms ease,
transform 150ms ease;
}
@media (prefers-reduced-motion: reduce) {
.floating-panel {
transition: none;
transform: none;
}
}
In Vue templates, you can also inspect window.matchMedia('(prefers-reduced-motion: reduce)') to conditionally disable JavaScript-driven transition durations.
Official specifications
- W3C Understanding WCAG 2.1 SC 1.4.13: Content on Hover or Focus: Detailed intent and test criteria for dismissable, hoverable, and persistent content.
- W3C Understanding WCAG 2.1 SC 2.1.1: Keyboard: Ensuring all functionality is operable through a keyboard interface.
- MDN Web Docs:
prefers-reduced-motion: Documentation on detecting user motion preferences.
Where to go next
- Focus Models: Learn how to manage modal focus containment, inert isolation, and return restoration.
- First Tooltip: Hands-on tutorial building an accessible tooltip with hover and focus.
useHoverAPI: Options for hover delay, rest timing, and safe polygons.useEscapeKeyAPI: Options and behavior for Escape key handling.