Skip to main content

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.

OptionTypeDefaultDescription
focusablebooleantrueWhether this component can receive focus. Set to false to temporarily disable a component without unmounting it.
saveLastFocusedChildbooleantrueWhen focus returns to this container, restore focus to the last focused child instead of the first.
trackChildrenbooleanfalseUpdate hasFocusedChild when any descendant gains or loses focus. Must be true to use hasFocusedChild for styling.
autoRestoreFocusbooleantrueIf this component is focused when it unmounts, automatically restore focus to the nearest other component.
forceFocusbooleanfalseMark this component as the preferred fallback target when focus is lost and no other candidate exists.
isFocusBoundarybooleanfalsePrevent focus from leaving this container in any direction. See Focus Boundaries.
focusBoundaryDirectionsDirection[]undefinedLimit boundary behavior to specific directions only (e.g., ['up', 'left']). Only used when isFocusBoundary is true.
focusKeystringauto-generatedA stable, unique identifier for this component. Required for programmatic focus via setFocus.
preferredChildFocusKeystringundefinedFocus key of the child that should receive focus when this container is first entered.
onEnterPressEnterPressHandler<P>no-opCalled when the Enter key is pressed while this component is focused.
onEnterReleaseEnterReleaseHandler<P>no-opCalled when the Enter key is released.
onArrowPressArrowPressHandler<P>() => trueCalled when an arrow key is pressed. Return true to allow default navigation, false to prevent it.
onArrowReleaseArrowReleaseHandler<P>no-opCalled when an arrow key is released.
onFocusFocusHandler<P>no-opCalled when this component gains focus.
onBlurBlurHandler<P>no-opCalled when this component loses focus.
extraPropsPundefinedArbitrary data passed as the first argument to all event callbacks. Use this to avoid closure stale-state issues.

Result: UseFocusableResult<E>

PropertyTypeDescription
refRefObject<E>Attach to the DOM element that represents this focusable. The library measures this element's position to perform navigation.
focusedbooleantrue when this exact component is the current focus target.
hasFocusedChildbooleantrue when any descendant of this component is focused. Only meaningful when trackChildren: true.
focusKeystringThe focus key in use (either the one you provided or the auto-generated one).
focusSelf(focusDetails?: FocusDetails) => voidProgrammatically 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 ref must 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, and preferredChildFocusKey are not reactive after mount. Callbacks are updated via a separate effect, but structural options like saveLastFocusedChild, trackChildren, autoRestoreFocus, and forceFocus are only read at registration time.
  • Use extraProps to pass data to callbacks instead of relying on closure variables. This avoids stale closure issues with callbacks that reference component props.