@usefy/use-focus-within
v0.25.1
Published
A React hook that tracks whether keyboard focus is anywhere within a subtree — focusin/focusout based, SSR-safe, with onFocus/onBlur edge callbacks
Maintainers
Readme
Overview
useFocusWithin is part of the @usefy ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It tells you, as React state, whether keyboard focus is currently on a container or any of its descendants — the reactive, state-driven equivalent of the CSS :focus-within pseudo-class.
Attach the returned callback ref to any element and read the boolean: highlight a form while it's being filled in, reveal a toolbar while a field group is active, or fire side effects when focus enters/leaves a region.
It's built on the bubbling focusin / focusout events (the non-bubbling focus / blur can't observe descendant focus), and correctly distinguishes focus moving between descendants — where focused stays true with no flicker — from focus leaving the subtree entirely, where it flips to false.
Features
[ref, focused]tuple — attach the callback ref, read the boolean; no wiring- No flicker — focus moving between two children keeps
focusedtrue(unlike naïvefocus/blurtracking) - Robust
relatedTarget: nullhandling — some browsers report anullrelatedTargeteven when focus stays inside; the hook defers todocument.activeElementon the next microtask to decide correctly - Edge callbacks — optional
onFocus/onBlurfire only on the subtree'sfalse ↔ truetransitions, never on inner focus moves; kept stable via a latest-ref so new inline handlers never re-subscribe listeners - Callback ref — listeners attach/detach exactly when the element mounts, unmounts, or the ref moves to another element (works on React 18 and 19)
- SSR-safe — no
window/documentaccess on the server; initialfocusedisfalse - StrictMode / concurrent-safe — no duplicate listeners, no stuck state on double mount
- TypeScript-first — full type inference and exported types
- Tiny & tree-shakeable — published as its own package
Installation
# npm
npm install @usefy/use-focus-within
# yarn
yarn add @usefy/use-focus-within
# pnpm
pnpm add @usefy/use-focus-withinRequires React 18 or 19 (peerDependencies: "react": "^18.0.0 || ^19.0.0").
Quick Start
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
useFocusWithin(options?)
const [ref, focused] = useFocusWithin<T>(options?);
// <div ref={ref}> …focusable content… </div>Returns a [ref, focused] tuple:
| Element | Type | Description |
| --------- | ----------------------------- | ----------------------------------------------------------------------------------------------- |
| ref | (node: T \| null) => void | A stable callback ref to attach to the container element you want to track. |
| focused | boolean | true 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
| Option | Type | Description |
| --------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| onFocus | (event: FocusEvent) => void | Fires when focus enters a previously-unfocused subtree. Receives the triggering focusin event. |
| onBlur | (event: FocusEvent) => void | Fires 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 thenullcases).- Types:
UseFocusWithinOptions,UseFocusWithinRef,UseFocusWithinReturn.
How focusout is resolved
On focusout the hook decides whether focus truly left the subtree:
- If
event.relatedTargetis a node inside the container → focus just moved between descendants; stayfocused. - If
relatedTargetis a node outside the container → focus left; gofalse. - If
relatedTargetisnull(unreliable — reported when focus goes to nothing, to another window, or by browsers that omit it) → defer one microtask and re-checkdocument.activeElement; gofalseonly 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.
Examples
Highlight a card while it's being filled in
const [ref, focused] = useFocusWithin<HTMLDivElement>();
return (
<div ref={ref} className={focused ? "card card--active" : "card"}>
<input placeholder="Card number" />
<input placeholder="Expiry" />
<input placeholder="CVC" />
</div>
);Run side effects on enter/leave
const [ref, focused] = useFocusWithin<HTMLDivElement>({
onFocus: () => analytics.track("filter_group_focused"),
onBlur: () => validateGroup(),
});onFocus/onBlur fire only on the subtree's transitions — tabbing across the inner fields won't re-fire them.
Reveal a toolbar while a region is active
const [ref, focused] = useFocusWithin<HTMLDivElement>();
return (
<div ref={ref}>
<textarea placeholder="Write something…" />
{focused && <FormattingToolbar />}
</div>
);Testing
📊 View Detailed Coverage Report (GitHub Pages) — 26 tests, 98% statement coverage.
useFocusWithin.test.tsx— hook behavior (focus in/out, container-level focus, no-flicker between descendants, leave to outside, blur-to-nothing;relatedTargetinside/outside/null- andundefined-quirk branches;onFocus/onBluredge semantics; latest-callback without re-subscribe; callback-ref attach/detach, initial-focus sync, detach reset, unmount cleanup; ref stability, SSR-inert default, StrictMode) plus theisFocusInsidepredicate in isolation.
License
MIT © mirunamu
This package is part of the usefy monorepo.
