All hooks / Events and input

useFocusWithin

266 B minified and brotliedSourceMarkdown✓ Works in all modern browsers

Whether focus is on the element or anything inside it, like CSS :focus-within, with callbacks when focus enters or leaves. Moving focus between children inside doesn't count as leaving.

Switching to another window or tab doesn't count either: focus is still there when the user comes back. Content in a React portal is outside in the DOM, so focus moving into it counts as leaving: put the ref on an element that contains the portal's target too.

Import

import { useFocusWithin } from '@vincent.ri/react-tils'
// or only this hook
import { useFocusWithin } from '@vincent.ri/react-tils/useFocusWithin'

Usage

const [open, setOpen] = useState(false)
const outsideRef = useClickOutside(() => setOpen(false), open)
const [focusRef] = useFocusWithin({ onLeave: () => setOpen(false) })
const ref = useMergedRefs(outsideRef, focusRef)
return (
  <div ref={ref}>
    <button onClick={() => setOpen(!open)}>Menu</button>
    {open && <ul>…</ul>}
  </div>
)

Live demo

Open the menu with the keyboard, then Tab past the last item.

focus inside: no · menu closed

When to use it

Closing a menu, popover, or combobox list when keyboard users Tab out of it, which useClickOutside alone misses; saving or collapsing an inline editor when focus leaves it; showing toolbar actions while a composer is focused.

When not to

Only styling; use CSS :focus-within. For a modal, keep focus inside with useFocusTrap instead. On iOS, tapping a button doesn't focus it, so pair this with useClickOutside for pointer users.

API

function useFocusWithin<T extends Element = HTMLElement>(options?: FocusWithinOptions): [(element: T | null) => void, boolean]
ParameterDescription
optionsonEnter, onLeave. Optional; can change every render.

Returns [ref, focused].

Types

export type FocusWithinOptions = {
  /** Focus moved into the element from outside. */
  onEnter?: () => void;
  /** Focus left the element and everything in it: Tab past the end, Shift+Tab, or a click elsewhere. */
  onLeave?: () => void;
};

Browser support

Uses only long-established browser features, so it works wherever React does.

First version with support, from MDN's compatibility data (web-features 3.42.0, @mdn/browser-compat-data 8.1.5).

Coming from another library?

Other libraries call this useFocusWithin (react-aria, Mantine), useFocusOutside.