Hooks
useHover
A callback-ref hover hook for driving non-visual side effects, not styling.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/use-hover.jsonPrompt
Build a React + TypeScript "useHover" hook (no dependencies beyond React).
Contract
- `useHover<T extends HTMLElement = HTMLElement>(): { ref: (node: T | null) =>
void; hovered: boolean }`.
- `ref` is a callback ref — attach it to the single element whose hover state
you want to track. No options object; the hook has no configurable
parameters.
Behavior
- The `ref` callback binds `pointerenter`/`pointerleave` listeners on the node
it receives, using Pointer Events (not `mouseenter`/`mouseleave`) so pen
input is covered alongside mouse. Touch input does not produce a real hover:
on touch-only devices `pointerenter` either never fires or fires briefly on
tap — that's platform semantics, not something the hook works around.
- `hovered` is only ever set inside the `pointerenter`/`pointerleave` event
callbacks (asynchronous, browser-triggered) — never synchronously during
render or inside a bare effect body.
- When the callback ref is invoked with a different node (element swapped via
a key/branch change) or with `null` (unmount), it removes the listeners from
the *previous* node (kept in a ref, not assumed to still be the argument)
and resets `hovered` to `false`, so a freshly mounted replacement node never
inherits a stale "hovered: true" from the node it replaced.
- Multiple components each calling `useHover()` own fully independent state —
no shared/module-level hover flag.
Rendering & styling
- The hook renders nothing and holds no opinion on styling. Consumers decide
what `hovered` drives — semantic tokens (`bg-primary`, `text-muted-foreground`)
for any visual feedback layered on top, `motion-reduce:` variants for any
transition tied to it.
Customization levers
- Focus-equivalent variant: if the same hover-driven logic must also fire on
keyboard focus (accessibility parity), also bind `focus`/`blur` on the node
inside the same callback ref and OR them into one `hovered` state — this
hook intentionally ships pointer-only by default so it never has to guess
whether a given hover use case wants focus parity.
- Delay threshold: wrap the consumer's side effect (not the hook itself) in a
`setTimeout` keyed off `hovered`, cancelled on cleanup if `hovered` flips
back to `false` before it fires — this keeps the hook itself a plain boolean
and lets each consumer pick its own delay (see the prefetch demo).
- This hook deliberately does NOT debounce or delay `hovered` itself, and does
NOT track pointer coordinates (see `useMousePosition` for that) — it is
purely a hover-boundary boolean.Concepts
- Callback-ref rebinding — the same pattern as
useIntersectionObserver: because React calls a callback ref on every mount/replace/unmount, the hook always knows exactly which node it's bound to, so swapping the tracked element (conditional render, key change) re-binds for free without a separate effect watching a ref object. - Pointer Events over mouse events —
pointerenter/pointerleavecover mouse and pen; touch does not produce a genuine hover and is intentionally not special-cased. - Hover as a trigger, not a style —
hoveredis meant to gate a side effect (prefetch, delayed popover, preview playback). Purely visual hover feedback belongs in CSS:hover/group-hover, never in this hook. - Stale-state guard on replacement — detaching the previous node's listeners and resetting
hoveredtofalseon everyrefcall (includingnull) prevents a node swap from leaving a phantom "hovered" state behind. - Not a focus equivalent — keyboard users never trigger
pointerenter; a hover-driven interaction that must also be keyboard-reachable needs an explicitfocus/blurmerge on top of this hook.
usePrevious
A hook that returns the last distinct value (Object.is) before the current one, via render-time state adjustment instead of a ref+effect.
useEventListener
A type-safe window/document/element event listener hook with a latest-ref handler, so it never stale-closures and never rebinds on handler change.