mcpField
N
norigin-spatial-navigation-react

norigin-spatial-navigation-react

github.com/noriginmediaDevelopment49391 installsUpdated 2d ago
Repository

About norigin-spatial-navigation-react

norigin-spatial-navigation-react is a skill for using Norigin Spatial Navigation to manage arrow-key and remote-control focus in React apps. It explains focusable elements, nested containers, boundaries, and programmatic focus for interfaces such as TV apps.

Key features

  1. 01Spatial focus navigation for React
  2. 02Focusable leaves and hierarchical containers
  3. 03Focus boundaries for modals and overlays
  4. 04Programmatic focus and stable focus keys
  5. 05Custom directional input and event callbacks

Use cases

  1. 01Building arrow-key navigation for Smart TV interfaces
  2. 02Managing focus in menus, grids, and carousels
  3. 03Trapping focus in modal dialogs
  4. 04Adapting navigation to custom remote keys

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/noriginmedia/norigin-spatial-navigation --skill norigin-spatial-navigation-react

After installing, type/norigin-spatial-navigation-reactin Claude Code to confirm it loaded.

Details

From the project README

Norigin Spatial Navigation

Library for arrow-key / remote-control focus management in React apps (Smart TVs, set-top boxes, browsers). It calculates the next focusable element based on spatial position, so you don't wire up directional logic manually.

Full documentation: https://github.com/NoriginMedia/Norigin-Spatial-Navigation/tree/main/docs

When you need details beyond this skill, read the relevant page directly:

  • Concepts & hierarchy: docs/guides/concepts.md, docs/guides/focus-hierarchy.md
  • Quick start & install: docs/guides/quick-start.md, docs/guides/installation.md
  • useFocusable API: docs/api-reference/useFocusable.md
  • SpatialNavigation API (init, setFocus, pause, etc.): docs/api-reference/SpatialNavigation.md
  • Boundaries / modals: docs/guides/focus-boundaries.md
  • Programmatic focus: docs/guides/programmatic-focus.md
  • Distance calculation tuning: docs/guides/distance-calculation.md
  • Key mapping (custom remotes): docs/guides/key-mapping.md
  • Event callbacks: docs/guides/event-callbacks.md
  • Performance: docs/guides/performance.md
  • Debugging: docs/guides/debugging.md
  • Recipes (lists, grids, virtualization): docs/guides/recipes.md
  • Accessibility labels: docs/guides/accessibility-labels.md
  • RTL: docs/guides/rtl-support.md

Mental model

Two component shapes exist:

  1. Leaf — interactive element that actually receives focus. Reads focused from useFocusable.
  2. Container — wraps focusable children. Reads focusKey and provides it via FocusContext.Provider so the children participate in the hierarchy. Optionally reads hasFocusedChild (requires trackChildren: true).

Every focusable component must attach the returned ref to a real DOM element. Containers must wrap children in <FocusContext.Provider value={focusKey}> — otherwise children are orphaned at the root and navigation across them breaks.

Initialization

Call init once at app startup (e.g. in App.tsx):

import { init as initNavigation } from '@noriginmedia/norigin-spatial-navigation-core';

initNavigation({
  debug: false,
  visualDebug: false,
  throttle: 0,
  distanceCalculationMethod: 'corners' // 'center' | 'edges' | 'corners'
});

If you ship custom remote keys, also configure key mapping (see docs/guides/key-mapping.md).

Patterns

Leaf (button, menu item)
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation-react';

function Button({ label, onPress }: Props) {
  const { ref, focused } = useFocusable({ onEnterPress: onPress });
  return (
    <div ref={ref} className={focused ? 'focused' : ''}>
      {label}
    </div>
  );
}
Container
import {
  useFocusable,
  FocusContext
} from '@noriginmedia/norigin-spatial-navigation-react';

function Menu({ items }: Props) {
  const { ref, focusKey, hasFocusedChild } = useFocusable({
    trackChildren: true,
    saveLastFocusedChild: true
  });

  return (
    <FocusContext.Provider value={focusKey}>
      <div ref={ref} className={hasFocusedChild ? 'active' : ''}>
        {items.map((i) => (
          <MenuItem key={i.id} {...i} />
        ))}
      </div>
    </FocusContext.Provider>
  );
}
Modal / popup (trap focus + grab on mount)
function Popup() {
  const { ref, focusKey, focusSelf } = useFocusable({ isFocusBoundary: true });

  useEffect(() => {
    focusSelf();
  }, [focusSelf]);

  return (
    <FocusContext.Provider value={focusKey}>
      <div ref={ref}>{/* buttons */}</div>
    </FocusContext.Provider>
  );
}

For partial trapping (e.g. block only horizontal), use focusBoundaryDirections. See docs/guides/focus-boundaries.md.

Programmatic focus from anywhere
import {
  setFocus,
  doesFocusableExist
} from '@noriginmedia/norigin-spatial-navigation-core';

const KEY = 'primary-cta';
useFocusable({ focusKey: KEY });

if (doesFocusableExist(KEY)) setFocus(KEY);
Conditional focusability
const { ref } = useFocusable({ focusable: !disabled });
Blocking / overriding directional input

onArrowPress(direction, props) — return true to allow default navigation, false to block it. Useful for paginated rows, custom carousels, or refusing to leave the current container at edges.

useFocusable — params worth knowing

ParamPurpose
focusableToggle whether element participates in navigation. Default true.
trackChildrenEnables hasFocusedChild. Has a perf cost — only enable when you read it.
saveLastFocusedChildContainer restores last focused child when re-entered. Default true.
isFocusBoundary / focusBoundaryDirectionsTrap focus inside the container.
focusKeyStable key for setFocus / doesFocusableExist.
preferredChildFocusKeyWhich child gets initial focus when container is entered.
onEnterPress, onArrowPress, onFocus, onBlurEvent hooks. See docs/guides/event-callbacks.md.
extraPropsArbitrary payload forwarded to callbacks (e.g. item id).

Returned: ref (required), focusKey, focused (leaf), hasFocusedChild (container), focusSelf().

Top-level API surface

import {
  init,
  setFocus,
  getCurrentFocusKey,
  doesFocusableExist,
  navigateByDirection,
  pause,
  resume,
  setKeyMap,
  setThrottle,
  destroy
} from '@noriginmedia/norigin-spatial-navigation-core';

See docs/api-reference/SpatialNavigation.md for the full list and signatures.

Best practices

Do

  • Always attach ref to a DOM element.
  • Wrap container children in <FocusContext.Provider value={focusKey}>.
  • Enable trackChildren only when you actually consume hasFocusedChild.
  • Use stable focusKeys for elements you target programmatically.
  • Guard setFocus calls with doesFocusableExist, especially around route changes / async data.
  • Use isFocusBoundary for modals, drawers, and overlays.
  • Set focusable: false on disabled or hidden items rather than unmounting if they may toggle frequently.

Don't

  • Don't read focused on a container — use hasFocusedChild instead.
  • Don't omit FocusContext.Provider on containers; children become unreachable from siblings.
  • Don't blindly call setFocus on keys that may not be mounted (causes silent focus loss).
  • Don't generate focusKeys that change on every render — focus state will be lost.
  • Don't enable debug / visualDebug in production builds.

Debugging

Turn on diagnostics in init:

init({ debug: true, visualDebug: true });

Common issues:

  • Element won't focus — ref not attached, or focusable: false, or ancestor missing FocusContext.Provider.
  • Focus jumps to wrong neighbor — try a different distanceCalculationMethod (corners / center / edges); enable visualDebug to see hitboxes.
  • Focus lost on re-render — focusKey is unstable, or the previously focused node unmounted without a fallback. Use preferredChildFocusKey or setFocus after the new tree mounts.
  • Modal leaks focus — missing isFocusBoundary on the modal container.

More: docs/guides/debugging.md, docs/guides/performance.md.

Quick reference

// Leaf
const { ref, focused } = useFocusable();

// Container
const { ref, focusKey } = useFocusable();
<FocusContext.Provider value={focusKey}>
  <div ref={ref}>{children}</div>
</FocusContext.Provider>;

// Focus on mount
const { focusSelf } = useFocusable({ isFocusBoundary: true });
useEffect(() => {
  focusSelf();
}, [focusSelf]);

// Manual focus
if (doesFocusableExist('my-key')) setFocus('my-key');

Comments

Markdown supported