useFocusable
useFocusable is the primary React hook for making a component navigable. It registers the component with the spatial navigation service, tracks focus state, and surfaces event callbacks.
Signature
function useFocusable<P = object, E = any>(
config?: UseFocusableConfig<P>
): UseFocusableResult<E>;
The generic parameter P is the type of extraProps. The parameter E is the type of the DOM element referenced by ref (defaults to any).
Configuration: UseFocusableConfig<P>
All options are optional.
| Option | Type | Default | Description |
|---|---|---|---|
focusable | boolean | true | Whether this component can receive focus. Set to false to temporarily disable a component without unmounting it. |
saveLastFocusedChild | boolean | true | When focus returns to this container, restore focus to the last focused child instead of the first. |
trackChildren | boolean | false | Update hasFocusedChild when any descendant gains or loses focus. Must be true to use hasFocusedChild for styling. |
autoRestoreFocus | boolean | true | If this component is focused when it unmounts, automatically restore focus to the nearest other component. |
forceFocus | boolean | false | Mark this component as the preferred fallback target when focus is lost and no other candidate exists. |
isFocusBoundary | boolean | false | Prevent focus from leaving this container in any direction. See Focus Boundaries. |
focusBoundaryDirections | Direction[] | undefined | Limit boundary behavior to specific directions only (e.g., ['up', 'left']). Only used when isFocusBoundary is true. |
focusKey | string | auto-generated | A stable, unique identifier for this component. Required for programmatic focus via setFocus. |
preferredChildFocusKey | string | undefined | Focus key of the child that should receive focus when this container is first entered. |
onEnterPress | EnterPressHandler<P> | no-op | Called when the Enter key is pressed while this component is focused. |
onEnterRelease | EnterReleaseHandler<P> | no-op | Called when the Enter key is released. |
onArrowPress | ArrowPressHandler<P> | () => true | Called when an arrow key is pressed. Return true to allow default navigation, false to prevent it. |
onArrowRelease | ArrowReleaseHandler<P> | no-op | Called when an arrow key is released. |
onFocus | FocusHandler<P> | no-op | Called when this component gains focus. |
onBlur | BlurHandler<P> | no-op | Called when this component loses focus. |
extraProps | P | undefined | Arbitrary data passed as the first argument to all event callbacks. Use this to avoid closure stale-state issues. |
Result: UseFocusableResult<E>
| Property | Type | Description |
|---|---|---|
ref | RefObject<E> | Attach to the DOM element that represents this focusable. The library measures this element's position to perform navigation. |
focused | boolean | true when this exact component is the current focus target. |
hasFocusedChild | boolean | true when any descendant of this component is focused. Only meaningful when trackChildren: true. |
focusKey | string | The focus key in use (either the one you provided or the auto-generated one). |
focusSelf | (focusDetails?: FocusDetails) => void | Programmatically focus this component. Equivalent to calling setFocus(focusKey). |
Handler Type Signatures
type EnterPressHandler<P = object> = (
props: P,
details: KeyPressDetails
) => void;
type EnterReleaseHandler<P = object> = (props: P) => void;
type ArrowPressHandler<P = object> = (
direction: string,
props: P,
details: KeyPressDetails
) => boolean;
type ArrowReleaseHandler<P = object> = (direction: string, props: P) => void;
type FocusHandler<P = object> = (
layout: FocusableComponentLayout,
props: P,
details: FocusDetails
) => void;
type BlurHandler<P = object> = (
layout: FocusableComponentLayout,
props: P,
details: FocusDetails
) => void;
See Event Callbacks for the full type definitions of KeyPressDetails, FocusDetails, and FocusableComponentLayout.
Usage Examples
Minimal leaf component
function Button({ label }: { label: string }) {
const { ref, focused } = useFocusable();
return (
<button ref={ref} style={{ outline: focused ? '2px solid white' : 'none' }}>
{label}
</button>
);
}
Component with all callbacks
import {
useFocusable,
FocusableComponentLayout,
KeyPressDetails,
FocusDetails
} from '@noriginmedia/norigin-spatial-navigation-react';
interface CardProps {
id: string;
title: string;
}
function Card({ id, title }: CardProps) {
const { ref, focused } = useFocusable<CardProps>({
focusKey: `card-${id}`,
extraProps: { id, title },
onFocus: (
layout: FocusableComponentLayout,
props: CardProps,
details: FocusDetails
) => {
console.log(
'Focused card:',
props.title,
'at position',
layout.x,
layout.y
);
},
onBlur: (layout: FocusableComponentLayout, props: CardProps) => {
console.log('Blurred card:', props.title);
},
onEnterPress: (props: CardProps, details: KeyPressDetails) => {
console.log('Enter pressed on:', props.title);
},
onEnterRelease: (props: CardProps) => {
console.log('Enter released on:', props.title);
},
onArrowPress: (
direction: string,
props: CardProps,
details: KeyPressDetails
) => {
console.log('Arrow pressed:', direction, 'on', props.title);
return true; // allow default navigation
},
onArrowRelease: (direction: string, props: CardProps) => {
console.log('Arrow released:', direction, 'on', props.title);
}
});
return (
<div ref={ref} style={{ outline: focused ? '2px solid white' : 'none' }}>
{title}
</div>
);
}
Container with child tracking
import {
useFocusable,
FocusContext
} from '@noriginmedia/norigin-spatial-navigation-react';
function Row({ title }: { title: string }) {
const { ref, focusKey, hasFocusedChild } = useFocusable({
trackChildren: true,
saveLastFocusedChild: true
});
return (
<FocusContext.Provider value={focusKey}>
<div>
<h2 style={{ color: hasFocusedChild ? 'white' : 'gray' }}>{title}</h2>
<div ref={ref} style={{ display: 'flex', gap: '12px' }}>
<Card id="1" title="Item 1" />
<Card id="2" title="Item 2" />
<Card id="3" title="Item 3" />
</div>
</div>
</FocusContext.Provider>
);
}
Temporarily disabling focus
function Button({ label, disabled }: { label: string; disabled: boolean }) {
const { ref, focused } = useFocusable({
focusable: !disabled // navigation skips this component when disabled
});
return (
<button
ref={ref}
disabled={disabled}
style={{ opacity: disabled ? 0.5 : 1 }}
>
{label}
</button>
);
}
Using focusSelf to self-focus on mount
import { useEffect } from 'react';
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation-react';
function Modal() {
const { ref, focusKey, focusSelf } = useFocusable({
focusKey: 'MODAL',
isFocusBoundary: true
});
useEffect(() => {
focusSelf();
}, [focusSelf]);
return <div ref={ref}>{/* modal content */}</div>;
}
Notes
- The
refmust be attached to a DOM element that has non-zero width and height when the component mounts. If the element is zero-sized, the library cannot measure its position and navigation to/from it will not work correctly. - Config options other than
focusKey,focusable,isFocusBoundary,focusBoundaryDirections, andpreferredChildFocusKeyare not reactive after mount. Callbacks are updated via a separate effect, but structural options likesaveLastFocusedChild,trackChildren,autoRestoreFocus, andforceFocusare only read at registration time. - Use
extraPropsto pass data to callbacks instead of relying on closure variables. This avoids stale closure issues with callbacks that reference component props.