All hooks / Events and input
useFocusWithin
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.
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]| Parameter | Description |
|---|---|
options | onEnter, 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.