Skip to content
usefy

useFocusWithin

Events & gestures

Track whether keyboard focus is currently anywhere within a subtree — the reactive equivalent of CSS <code>:focus-within</code>

Install

$ npm install @usefy/use-focus-within

Quick start

useFocusWithin.tsx
import { useFocusWithin } from "@usefy/use-focus-within";

function ContactForm() {
  const [ref, focused] = useFocusWithin<HTMLFormElement>();

  return (
    <form
      ref={ref}
      style={{ outline: focused ? "2px solid dodgerblue" : "none" }}
    >
      <input placeholder="Name" />
      <input placeholder="Email" />
      <button type="submit">Send</button>
    </form>
  );
}

API reference

useFocusWithin(options?)

const [ref, focused] = useFocusWithin<T>(options?);
// <div ref={ref}> …focusable content… </div>

Returns a [ref, focused] tuple:

ElementTypeDescription
ref(node: T | null) => voidA stable callback ref to attach to the container element you want to track.
focusedbooleantrue whenever the active element is the container or any descendant. Starts false (also on the server).

T defaults to HTMLElement; pass the concrete element type (e.g. HTMLFormElement) for a precisely-typed ref.

Options — UseFocusWithinOptions

OptionTypeDescription
onFocus(event: FocusEvent) => voidFires when focus enters a previously-unfocused subtree. Receives the triggering focusin event.
onBlur(event: FocusEvent) => voidFires when focus leaves the subtree entirely. Receives the triggering focusout event.

Both callbacks fire only on the edge transitions — moving focus from one descendant to another fires neither.

Also exported

  • isFocusInside(container, target) — the reusable predicate for "is this node the container or a descendant of it" (folds in the null cases).
  • Types: UseFocusWithinOptions, UseFocusWithinRef, UseFocusWithinReturn.

How focusout is resolved

On focusout the hook decides whether focus truly left the subtree:

  1. If event.relatedTarget is a node inside the container → focus just moved between descendants; stay focused.
  2. If relatedTarget is a node outside the container → focus left; go false.
  3. If relatedTarget is null (unreliable — reported when focus goes to nothing, to another window, or by browsers that omit it) → defer one microtask and re-check document.activeElement; go false only if focus genuinely ended up outside.

Step 3 is what makes the hook robust to the well-known null-relatedTarget quirk while staying fully testable in jsdom.

Go deeper

This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.

More in events & gestures