hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn
useSafeArea
usePresence

useSafeArea#

useSafeArea keeps a hover-opened floating element open while the pointer travels the empty gap toward it. When the pointer leaves the trigger it would normally fire pointerleave and close the element before the pointer arrives. This hook watches pointermove and treats the trigger, the floating element, and a safe corridor between them, a quad spanning the trigger's edge to the floating element's near edge, as one safe region: while the pointer rests anywhere inside it the element stays open, including when it dwells in the gap. Once the pointer leaves that region the element closes after a short grace period, which a move back inside cancels.

See also: usePosition, useDismiss, Tooltip.

Demo#

Example#

import { useSafeArea } from 'hono-preact-ui';
import { useRef } from 'preact/hooks';

function HoverCard({ open, onClose }: { open: boolean; onClose: () => void }) {
  const anchorRef = useRef<HTMLButtonElement>(null);
  const floatingRef = useRef<HTMLDivElement>(null);

  useSafeArea({ enabled: open, anchorRef, floatingRef, onClose });

  return (
    <>
      <button ref={anchorRef}>Profile</button>
      {open && <div ref={floatingRef}>card content</div>}
    </>
  );
}

How it works#

TriggerFloating element
Through the corridor: stays openOut of the corridor: closes

The safe corridor is a quad joining the trigger's near edge to the floating element's near edge, spanning the gap between them. While the element is open, a pointermove that stays inside the corridor, even on a diagonal that does not aim straight at the element, keeps it open, and so does pausing there. A move that leaves the corridor without reaching the floating element starts the grace timer and then closes the element; returning to the corridor or either element before the grace lapses cancels the close. The point-in-polygon test runs against the live rects of both elements on every move, so the corridor follows them through scroll, resize, and a flipped placement.

Signature#

import { useSafeArea } from 'hono-preact-ui';

function useSafeArea(opts: UseSafeAreaOptions): void;

interface UseSafeAreaOptions {
  enabled: boolean; // typically the open state of a hover-driven element
  anchorRef: RefObject<HTMLElement>; // the trigger the corridor starts from
  floatingRef: RefObject<HTMLElement>; // the floating element it points at
  onClose: () => void; // pointer left the safe region and the grace lapsed
  graceMs?: number; // close grace after leaving the safe region, default 300
}

Options#

OptionTypeDefaultNotes
enabledbooleannoneWatch the pointer only while the element is open.
anchorRefRefObject<HTMLElement>noneThe trigger the corridor starts from.
floatingRefRefObject<HTMLElement>noneThe floating element the corridor points at.
onClose() => voidnoneCalled after the pointer leaves the safe region and the grace period lapses.
graceMsnumber300Grace period after the pointer leaves the safe region before onClose fires; re-entering cancels it.

It injects no DOM: it listens to pointermove while enabled and runs a point-in-polygon test against the live rects of the two elements, so nothing under the corridor is blocked from interaction. The corridor follows the elements through scroll, resize, and a flipped placement. A pointer session begins only once the pointer has been over the trigger or the floating element, so an element opened by keyboard focus is not closed by unrelated mouse movement (the consumer closes it on blur or Escape instead). If the pointer leaves the document while a session is active, the same grace timer starts, so the element does not hang open when the cursor exits the window. Touch pointers are ignored.