Skip to content

fix(hover-card): keep open while pointer crosses gap between trigger and content - #3209

Open
Arman-Luthra wants to merge 1 commit into
chakra-ui:mainfrom
Arman-Luthra:fix/hover-card-grace-area
Open

fix(hover-card): keep open while pointer crosses gap between trigger and content#3209
Arman-Luthra wants to merge 1 commit into
chakra-ui:mainfrom
Arman-Luthra:fix/hover-card-grace-area

Conversation

@Arman-Luthra

Copy link
Copy Markdown
Contributor

Closes #3181

📝 Description

When positioning.gutter (or an offset) creates a visual gap between the trigger and the content, moving the pointer across that gap can close the hover card, because the pointer briefly hovers neither element. This PR adds a grace area (safe polygon) between the trigger and the content, similar to Floating UI's safePolygon and Radix's hover card.

⛳️ Current behavior (updates)

  • On pointerleave, the machine enters closing and closes after closeDelay, regardless of where the pointer is heading.
  • Crossing the gutter gap works only if the crossing takes less than closeDelay, so users are forced to increase closeDelay globally to make interactive hover cards usable with a gutter.

🚀 New behavior

  • When the pointer leaves the trigger towards the content (or leaves the content towards the trigger), a grace area polygon is computed from the exit point and the target element's rect (reusing getElementPolygon/isPointInPolygon from @zag-js/rect-utils, the same primitives the menu machine uses for its submenu intent polygon).
  • While the pointer stays inside the grace area, the CLOSE_DELAY expiry is ignored, so the card stays open during the traversal no matter how small closeDelay is.
  • As soon as the pointer exits the grace area (or leaves the document), the closing state re-enters and the regular closeDelay countdown restarts — so moving away still closes the card with the usual delay.
  • The grace area is only created when the pointer actually exits on the side facing the target element. Leaving in any other direction (e.g. jumping to an unrelated part of the page) keeps the previous close behavior.

Interaction with closeDelay: the delay is effectively paused while the pointer is in transit between the two elements, and restarts once the pointer leaves the corridor. Entering the content/trigger cancels the close as before.

💣 Is this a breaking change (Yes/No):

No. Public API is unchanged; the grace area is internal (refs). The only new dependency is the existing workspace package @zag-js/rect-utils.

📝 Additional Information

  • Added three e2e tests: slow traversal trigger → content and content → trigger through the gutter (both fail on main, pass with this fix), plus a test asserting the card still closes when the pointer enters the gap and then veers away.
  • Limitation (matches Radix behavior): if the pointer stops inside the gap and never moves again, the card stays open until the pointer moves out of the grace area. Leaving the document while in the gap is treated as exiting the grace area.
  • The grace area is intentionally not created for diagonal exits through a non-facing side (e.g. leaving the trigger's right edge when the content sits below); those fall back to the current closeDelay behavior.

@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 68dba5a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 86 packages
Name Type
@zag-js/hover-card Patch
@zag-js/anatomy-icons Patch
@zag-js/anatomy Patch
@zag-js/core Patch
@zag-js/docs Patch
@zag-js/preact Patch
@zag-js/react Patch
@zag-js/solid Patch
@zag-js/svelte Patch
@zag-js/vanilla Patch
@zag-js/vue Patch
@zag-js/accordion Patch
@zag-js/angle-slider Patch
@zag-js/async-list Patch
@zag-js/avatar Patch
@zag-js/carousel Patch
@zag-js/cascade-select Patch
@zag-js/checkbox Patch
@zag-js/clipboard Patch
@zag-js/collapsible Patch
@zag-js/color-picker Patch
@zag-js/combobox Patch
@zag-js/date-input Patch
@zag-js/date-picker Patch
@zag-js/dialog Patch
@zag-js/drawer Patch
@zag-js/editable Patch
@zag-js/file-upload Patch
@zag-js/floating-panel Patch
@zag-js/image-cropper Patch
@zag-js/listbox Patch
@zag-js/marquee Patch
@zag-js/menu Patch
@zag-js/navigation-menu Patch
@zag-js/number-input Patch
@zag-js/pagination Patch
@zag-js/password-input Patch
@zag-js/pin-input Patch
@zag-js/popover Patch
@zag-js/presence Patch
@zag-js/progress Patch
@zag-js/qr-code Patch
@zag-js/radio-group Patch
@zag-js/rating-group Patch
@zag-js/scroll-area Patch
@zag-js/select Patch
@zag-js/signature-pad Patch
@zag-js/slider Patch
@zag-js/splitter Patch
@zag-js/steps Patch
@zag-js/switch Patch
@zag-js/tabs Patch
@zag-js/tags-input Patch
@zag-js/timer Patch
@zag-js/toast Patch
@zag-js/toc Patch
@zag-js/toggle-group Patch
@zag-js/toggle Patch
@zag-js/tooltip Patch
@zag-js/tour Patch
@zag-js/tree-view Patch
@zag-js/store Patch
@zag-js/types Patch
@zag-js/aria-hidden Patch
@zag-js/auto-resize Patch
@zag-js/collection Patch
@zag-js/color-utils Patch
@zag-js/utils Patch
@zag-js/date-utils Patch
@zag-js/dismissable Patch
@zag-js/dom-query Patch
@zag-js/file-utils Patch
@zag-js/focus-trap Patch
@zag-js/focus-visible Patch
@zag-js/highlight-word Patch
@zag-js/hotkeys Patch
@zag-js/i18n-utils Patch
@zag-js/interact-outside Patch
@zag-js/json-tree-utils Patch
@zag-js/live-region Patch
@zag-js/popper Patch
@zag-js/rect-utils Patch
@zag-js/remove-scroll Patch
@zag-js/scroll-snap Patch
@zag-js/stringify-state Patch
svelte-kit-starter Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zag-nextjs Ready Ready Preview Jul 10, 2026 10:59pm
zag-solid Ready Ready Preview Jul 10, 2026 10:59pm
zag-svelte Ready Ready Preview Jul 10, 2026 10:59pm
zag-vue Ready Ready Preview Jul 10, 2026 10:59pm
zag-website Ready Ready Preview Jul 10, 2026 10:59pm

Request Review

@kalisaNkevin

kalisaNkevin commented Jul 27, 2026

Copy link
Copy Markdown
Member

Took a closer look at this one. The fix and tests hold up: the default gutter (8px, from @zag-js/popper's get-placement.ts:16) is enough for the original bug to reproduce, and the new e2e tests wait longer than the default 300ms closeDelay (≥600ms of traversal), so they genuinely fail on main and pass here — not just timing luck. The grace-area approach also isn't a new pattern here; it reuses getElementPolygon/isPointInPolygon from @zag-js/rect-utils, the same primitives menu.utils.ts already uses for submenu intent detection.

Two small nits, nothing blocking:

  • The JSDoc on createGraceArea (hover-card.utils.ts:15-24) has four @param lines that just restate the parameter names — worth trimming, since nothing else nearby (getElementPolygon, menu.utils.ts's isWithinPolygon) carries JSDoc like this.
  • The comment in onPointerLeave ("pointer left the document...") repeats almost the same wording as the comment on GRACE_AREA.EXIT a few lines away — could just say it once.

Looks good to merge otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HoverCard closes when moving pointer across gutter gap between trigger and content

2 participants