Skip to content
Zap Studio
react-hooks
Esc
navigateopen⌘Jpreview
On this page

DOM / Element Interaction

Ref'd-element observers, drag-and-drop, and browser chrome APIs — click-outside, intersection/resize/mutation observers, fullscreen, popovers, and more.

Hooks that observe a ref’d DOM element (visibility, size, mutations, hover, drag state) or reach into browser chrome (favicon, fullscreen, script loading, the clipboard-adjacent EyeDropper). Every hook here degrades to an inert, SSR-safe default when its underlying API is unsupported.

useClickOutside

Calls onOutside on a mousedown/touchstart whose target falls outside the ref’d element — the standard “close on outside click” pattern for dropdowns, popovers, and modals. Listens on the capture phase, so it still fires even if an inner handler calls stopPropagation().

const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
return open ? <div ref={ref}>Menu</div> : null;

useHover

Boolean hover state for a single ref’d element, via mouseenter/mouseleave.

const { ref, hovered } = useHover<HTMLDivElement>();
return <div ref={ref}>{hovered ? "Hovering" : "Not hovering"}</div>;

useMousePosition

Tracks the pointer’s clientX/clientY (plus pageX/pageY and screenX/screenY) via window’s mousemove event. There’s no synchronous read for pointer position, only the event, so this starts at all-0 until the pointer first moves.

const { clientX, clientY } = useMousePosition();

useIntersectionObserver

Tracks whether the ref’d element is visible in the viewport (or a given root), via IntersectionObserver. options is passed straight through to the observer (root/rootMargin/threshold). Also exported as useInView, an alias for the same hook.

const { ref, inView } = useIntersectionObserver<HTMLDivElement>();
return <div ref={ref}>{inView ? "Visible" : "Off-screen"}</div>;

useResizeObserver

Tracks the ref’d element’s content-box size via ResizeObserver.

const { ref, size } = useResizeObserver<HTMLDivElement>();
return (
  <div ref={ref}>
    {size?.width}×{size?.height}
  </div>
);

useEventListener

Typed addEventListener wrapper with automatic cleanup — attaches handler for type on target (a RefObject, a DOM node, or window/document), and removes it on unmount or when the resolved element, type, or options change. Neither handler nor options needs to be memoized: the latest handler is always called without re-subscribing, and options is compared field by field, so an object literal written inline at the call site is free.

The listener is attached in a layout effect (useIsomorphicLayoutEffect), before the browser paints, so no event can slip through the gap a passive effect would leave open. When target is a ref, its current is re-read on every commit — a ref that is still null on the first render, or that later points at a different element, is picked up as soon as React commits the change.

const ref = useRef<HTMLDivElement>(null);
useEventListener(ref, "scroll", (event) => console.log(event));
useEventListener(window, "resize", () => console.log("resized"));

useLockBodyScroll

Locks document.body’s scroll while locked is true — the standard “no background scroll behind an open modal/drawer” pattern. Restores the body’s previous inline overflow on unlock or unmount.

useLockBodyScroll(isModalOpen);

useFavicon

Imperatively swaps the <link rel="icon"> href — creates the tag if the document doesn’t already have one, and restores the previous href on unmount.

useFavicon(hasUnread ? "/favicon-unread.svg" : "/favicon.svg");

useScript

Loads an external <script src> on demand. Concurrent useScript calls for the same src share a single <script> tag — the request is never duplicated. status starts "loading" and becomes "ready"/"error" once the script settles. With removeOnUnmount: true, the tag is removed once the last consumer of that src unmounts.

const { status } = useScript("https://maps.example.com/sdk.js");
if (status === "ready") renderMap();

useScrollPosition

window.scrollX/scrollY, updating on the scroll event — window-level; for a specific scrollable element, attach a listener to its ref instead (see useEventListener).

const { y } = useScrollPosition();
const showBackToTop = y > 400;

useTextSelection

The current page text selection, via window.getSelection(), updating on the document’s selectionchange event.

const text = useTextSelection();
const wordCount = text.trim().split(/\s+/).filter(Boolean).length;

useFilePicker

Wraps the File System Access APIshowOpenFilePicker/showSaveFilePicker/showDirectoryPicker. Each method resolves undefined (rather than throwing) when the user dismisses the native picker.

Chromium-only, no Safari/Firefox support yet — see MDN’s browser compatibility table. supported: false elsewhere, and every method resolves undefined without opening a dialog.

const { showOpenFilePicker, supported } = useFilePicker();
const handles = supported ? await showOpenFilePicker({ multiple: true }) : undefined;

useFileDrop

Drag-and-drop file upload state for a single ref’d drop target, via the HTML Drag and Drop API. isOver tracks whether a drag is currently over the element; onDrop is called with the dropped File[]. Also exported as useDropzone, an alias for the same hook.

const { ref, isOver } = useFileDrop<HTMLDivElement>((files) => upload(files));
return <div ref={ref}>{isOver ? "Drop to upload" : "Drag files here"}</div>;

useMutationObserver

Observes DOM mutations on the ref’d element/subtree via MutationObserver. Defaults to watching attributes, character data, and the full child subtree; options overrides that.

const ref = useMutationObserver<HTMLDivElement>((mutations) => console.log(mutations));
return <div ref={ref}>{children}</div>;

useFullscreen

Fullscreen API wrapper for a single ref’d element — attach ref, then call enter()/exit()/toggle() imperatively (the API requires a user gesture). isFullscreen tracks whether that exact element currently holds fullscreen, via fullscreenchange.

const { ref, isFullscreen, toggle } = useFullscreen<HTMLDivElement>();
return (
  <div ref={ref}>
    <button onClick={toggle}>{isFullscreen ? "Exit" : "Enter"}</button>
  </div>
);

usePointer

Unified mouse/touch/pen position and pressure via Pointer events — a modern superset of a plain mousemove-based position hook. isDown reflects whether the primary pointer button/contact is currently active.

const { clientX, clientY, pointerType, isDown } = usePointer();

usePopover

Wraps the native Popover API (2024+ baseline) for a single ref’d element carrying the popover attribute — call show()/hide()/toggle() imperatively. isOpen tracks the element’s open state via its own toggle event, so it also stays in sync when the browser closes the popover itself (light-dismiss, Esc).

const { ref, isOpen, toggle } = usePopover<HTMLDivElement>();
return (
  <>
    <button onClick={toggle}>Menu</button>
    <div ref={ref} popover="auto">
      {isOpen ? "Open" : "Closed"}
    </div>
  </>
);

useViewTransition

Wraps document.startViewTransition() — runs callback (typically a DOM update) inside a native view transition, animating between the before/after states.

Not supported in every browser — see MDN’s browser compatibility table. Where unsupported, startTransition just calls callback directly and resolves once it settles — the recommended fallback, per the spec, since the DOM update itself still needs to happen.

const { startTransition } = useViewTransition();
const handleThemeChange = () => startTransition(() => setTheme("dark"));

useExperimentalContactPicker

Wraps the Contact Picker API (navigator.contacts) — select() shows the OS contact picker for the given properties, resolving the chosen contacts, or undefined if the user cancels or the API is unsupported. getProperties() resolves which properties this browser can actually retrieve.

Experimental per MDN, Chromium-only, requires a secure context and a user gesture — see MDN’s browser compatibility table.

const { select, supported } = useExperimentalContactPicker();
const contacts = supported ? await select(["name", "email"], { multiple: true }) : undefined;

useExperimentalEyeDropper

Wraps the EyeDropper API — a single-shot native color picker. open() resolves the picked color as an sRGBHex string (e.g. "#ff0000"), or undefined if the user cancels or the API is unsupported.

Experimental per MDN, Chromium-only, no Safari/Firefox support yet — see MDN’s browser compatibility table.

const { open, supported } = useExperimentalEyeDropper();
const hex = supported ? await open() : undefined;

See Also

Last updated on September 21, 2026

Was this page helpful?