All skills
callstackincubator avatar

/react-native-tv-best-practices

@fa0bad0 official

Reviews React Native TV apps for focus/D-pad navigation, 10-foot UI layout, TV playback/DRM integration, low-memory TV performance, and TV accessibility. Use when building, debugging, or reviewing react-native-tvos, Expo TV, Amazon Vega/Kepler, or React Native web TV targets where the issue depends on remote input, TV focus, TV packaging, TV hardware, or TV playback constraints.

Use this Skill: https://skilld.dev/gh/callstackincubator/agent-skills/react-native-tv-best-practices

This session only. Nothing lands on disk.

referencesfocus-management.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Focus Management

Focus is the core interaction model on TV. Every D-pad press sends focus from one element to another. When focus behaves as expected, users glide through the interface. When it doesn't, they get stuck or overshoot.

Quick Reference

  • Let the platform focus engine handle it — design layouts that are naturally focus-friendly before adding manual focus logic
  • Use TVFocusGuideView for complex layouts that don't naturally connect
  • Use hasTVPreferredFocus to set initial focus on screen load
  • Use focus traps for modals and overlays
  • Imperative focus (requestTVFocus()) should be a last resort

Platform Focus Engines

tvOS — Inferred Focus Engine

Apple's focus engine examines element positions and spatial proximity:

  • Searches for focusable views in the direction of input
  • Treats clusters as "focus islands"
  • Expects clean grid/alignment patterns — misaligned elements cause unexpected jumps
  • Supports diagonal movement and inertia-based swipes

Android TV — Explicit Directional Model

  • Focus moves to nearest visible item along pressed direction (Cartesian)
  • Supports nextFocusUp, nextFocusDown, nextFocusLeft, nextFocusRight props
  • More tolerant of irregular layouts
  • When no valid target exists, focus can disappear entirely

Vega OS

Works like Android TV using Cartesian focus management strategy.

TVFocusGuideView

Groups focusable elements so the system can remember last focused child or redirect focus intelligently.

const refSidebar = useRef(null);
const refGrid = useRef(null);
const [destinations, setDestinations] = useState([]);

// destinations takes resolved components (ref.current), not the ref objects.
// Build it AFTER mount: ref.current is null on first render and mutating a
// ref triggers no re-render, so a new array must be set into state.
useEffect(() => {
  setDestinations([refSidebar.current, refGrid.current].filter(Boolean));
}, []);

<TVFocusGuideView destinations={destinations}>
  <View style={{ flexDirection: 'row' }}>
    <Sidebar ref={refSidebar} />
    <ContentGrid ref={refGrid} />
  </View>
</TVFocusGuideView>

If Sidebar/ContentGrid are custom function components, they must accept the ref: on Vega OS (RN 0.72 / React 18) wrap them in forwardRef; on react-native-tvos with React 19 (RN 0.78+) ref can be a plain prop. Built-in components like TouchableOpacity accept refs on both.

Props

  • destinations — Array of Components (pass ref.current, not the ref) to register as focus targets. The guide updates only when this prop changes; if refs are null on first render, set them into state once mounted so a new array is passed
  • trapFocusUp/Down/Left/Right — Prevents focus from escaping in specified directions
  • autoFocus — Redirects focus to first focusable child; remembers last focused child on revisit

hasTVPreferredFocus

Tells the focus engine where to start on screen load:

<Pressable hasTVPreferredFocus onPress={startPlayback}>
  <Text>Start Watching</Text>
</Pressable>

Rules:

  • Avoid setting multiple hasTVPreferredFocus in the same view
  • Delay focus until data-dependent UI has rendered
  • Available on: View, Pressable, TouchableHighlight, TouchableOpacity, TextInput, Button, TVFocusGuideView

Focus Traps for Modals/Overlays

When modals open, focus must stay inside them:

<TVFocusGuideView trapFocusUp trapFocusDown trapFocusLeft trapFocusRight>
  <View>
    <Pressable hasTVPreferredFocus onPress={onConfirm}>
      <Text>Confirm</Text>
    </Pressable>
  </View>
</TVFocusGuideView>

For web-based platforms (Tizen, webOS), use @noriginmedia/norigin-spatial-navigation to replicate similar behavior.

Imperative Focus — Last Resort

useEffect(() => {
  if (lastFocusedRef.current?.requestTVFocus) {
    lastFocusedRef.current.requestTVFocus();
  } else if (lastFocusedRef.current?.focus) {
    lastFocusedRef.current.focus();
  }
}, [isActiveScreen]);

When imperative focus is needed:

  • Restoring focus when returning to a screen
  • Scrolling a list where next target isn't yet mounted

Prefer focusing a stable container (e.g., a TVFocusGuideView) rather than a granular element.

nextFocus* Props

nextFocusUp, nextFocusDown, nextFocusLeft, nextFocusRight set on View are honored natively by the directional (Cartesian) focus engines — Android TV, Fire TV, and Vega OS — and also by tvOS in current react-native-tvos. The tvOS caveat: if there is no focusable view in the specified direction, the override is ignored and the engine falls back to inferred (spatial) focus.

Default rule: prefer natural focus order and TVFocusGuideView for complex or shared layouts. Reach for nextFocus* only as a targeted override when that tvOS caveat is acceptable — not as the primary navigation strategy.

Debugging Focus Issues

Visualize Focus Movement

  • tvOS: Simulator → Debug > Toggle Focus Rectangle
  • Android TV: adb logcat and log focus changes
  • In-component: Add red borders on focus for visual debugging
<Pressable
  testID="playButton"
  style={({ focused }) => ({
    borderWidth: focused ? 2 : 0,
    borderColor: focused ? 'red' : 'transparent',
  })}
>
  <Text>Play</Text>
</Pressable>

Add Logs

<Pressable
  onFocus={() => console.log('Focused: playButton')}
  onBlur={() => console.log('Blurred: playButton')}
>

Use React DevTools

  • Inspect which components are actually focusable
  • Identify invisible/off-screen elements receiving focus
  • Profile re-renders after D-pad key presses

Common Gotchas

Issue Solution
No focusable element on screen Render a temporary focusable placeholder during loading
Focus lost after re-render Keep key values stable; restore focus after new item renders
Focus on hidden content Unmount hidden elements or disable focus explicitly
Gaps between elements Use TVFocusGuideView to bridge them
Wrong initial focus Only one hasTVPreferredFocus per view; wait for UI to render

Related Skills

Source: SKILL.md on GitHub

No alerts1mo3 checks · Risk SAFE
  • Gen Agent Trust Hub1mo

    This skill provides comprehensive documentation and best practices for developing React Native applications for TV platforms. It contains technical guidelines for focus management, performance, and video playback without any malicious patterns, obfuscation, or unsafe code execution.

  • Socket1mo

    No alerts

  • Snyk1mo

    Risk: LOW · No issues

Signed by skilld at fa0bad0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago

README badge

README badge for callstackincubator/agent-skills/react-native-tv-best-practices