agents/openai.yaml
interface:
display_name: "React Native TV Best Practices"
short_description: "Build and review React Native TV experiences"
default_prompt: "Use $react-native-tv-best-practices to build or review a React Native TV experience for focus, remote input, layout, playback, performance, and accessibility."
references/a11y-checklist.md
---
title: Accessibility Checklist for TV Apps
impact: MEDIUM
tags: accessibility, checklist, screen-reader, captions, focus, tv
---
# Accessibility Checklist for TV Apps
Pre-launch checklist for remote-only navigation, TV screen readers, captions, and focus-visible UI.
## 1. Navigation & Focus Management
- [ ] All interactive elements reachable via D-pad or remote
- [ ] Focus moves logically (left→right, top→bottom)
- [ ] Focus is visible and clearly indicated (highlight or border)
- [ ] No "focus traps" — areas where focus gets stuck or lost
- [ ] Back button behaves consistently and predictably
- [ ] Menus and modals trap focus while active and restore on close
## 2. Screen Reader & VoiceOver Support
- [ ] All interactive elements have clear, descriptive labels ("Play Button", "Episode 2: Stranger Things")
- [ ] Dynamic content changes announced via `accessibilityLiveRegion`
- [ ] Screen reader correctly reads button states (selected, disabled)
- [ ] Non-text elements (icons, images) have appropriate alt text/labels
- [ ] Dialogs ("Are you still watching?") announced and navigable via screen reader
## 3. Visual Design & Contrast
- [ ] Text size large enough for TV viewing (>18px minimum)
- [ ] Text and interactive elements ≥4.5:1 contrast ratio against background
- [ ] Focus indicators are high contrast and visible from distance
- [ ] No overlaid or animated text on busy video backgrounds
- [ ] No flashing or strobing elements (seizure risk)
## 4. Audio & Captions
- [ ] Captions/subtitles available for all video content
- [ ] Captions customizable: size, color, background
- [ ] Audio descriptions available and easy to enable/disable
- [ ] Action feedback ("Added to Watchlist") provided visually AND audibly
- [ ] Autoplay previews can be paused or disabled by user
## 5. Search & Content Discovery
- [ ] On-screen keyboard fully navigable with remote/D-pad
- [ ] Search suggestions readable and selectable via focus
- [ ] Search results announced to screen readers or at least navigable
- [ ] Search input has visible label or hint ("Search for shows or movies")
## 6. Dynamic Content & Live Updates
- [ ] New content in carousels/rows/lists is announced or focusable
- [ ] Lazy-loaded content doesn't reset or lose focus state
- [ ] Infinite scroll has clear separators/headings between content types
- [ ] Row headers ("Recommended", "Trending Now") announced as landmarks
## 7. Interactive Features (Like, Rate, Watchlist)
- [ ] Like/dislike/watchlist buttons focusable and labeled clearly
- [ ] Button states (liked, saved) announced to screen readers
- [ ] Confirmation messages both visual and accessible
- [ ] All icons (heart, star, thumb) have text equivalents
## 8. Testing & Platform-Specific
- [ ] Agent smoke-tested after loading the `agent-device` skill and reading `agent-device help workflow`; verified exposed labels, roles, states, focus, and modal focus behavior
- [ ] Manually tested spoken output on platform screen readers (TalkBack, VoiceOver, VoiceView) before release
- [ ] Reviewed against platform accessibility guidelines (tvOS HIG, Android TV UI)
## 9. Settings & User Control
- [ ] Accessibility settings (captions, audio description, high contrast) easily accessible
- [ ] Autoplay previews and audio can be disabled
- [ ] Users can control text size, color themes, contrast where supported
- [ ] App respects system-wide accessibility settings
## 10. Avoid These Common Mistakes
- [ ] No unlabeled buttons or icons ("button1")
- [ ] No focus on non-interactive elements (static labels)
- [ ] No inaccessible custom components (carousels without keyboard support)
- [ ] No functionality requiring complex remote combos or gestures only
## Testing Tools
| Tool | Type | Notes |
|------|------|-------|
| agent-device | Agent-run | Load the `agent-device` skill and read `agent-device help workflow`, then inspect accessibility tree, focused elements, labels, roles, states, and modal focus behavior |
| TalkBack | Manual | Android TV, Fire TV |
| VoiceOver | Manual | Apple TV |
| React Native Testing Library | Integration | Test accessibility props/labels |
| Accessibility Inspector | Manual | Xcode (tvOS) |
## Related Skills
- [a11y-overview.md](./a11y-overview.md) — Accessibility fundamentals
- [a11y-implementation.md](./a11y-implementation.md) — Implementation details
- [design-color.md](./design-color.md) — Color contrast guidelines
references/a11y-implementation.md
---
title: Accessibility Implementation in React Native TV
impact: HIGH
tags: accessibility, screen-reader, focus, voiceover, talkback, tv
---
# Accessibility Implementation in React Native TV
## Quick Reference
- For agent-run checks, load the `agent-device` skill first, then read `agent-device help workflow`
- Use `agent-device` accessibility-tree evidence to inspect exposed labels, roles, states, focus, and modal behavior
- Use manual screen-reader testing only for spoken-output timing, audio behavior, and platform quirks that automation cannot prove
- Do not put `accessible={true}` high in a modal/body tree; it collapses focusable children into one item
- Prefer `TVFocusGuideView` for shared focus paths; use `nextFocus*` only as a targeted override
- Await `AccessibilityInfo.isScreenReaderEnabled()` before muting autoplay or changing announcements
- Pair visual-only remote feedback with an accessibility announcement when state changes
## Agent-Run Accessibility Smoke
Before planning or running device automation, load the `agent-device` skill and follow its setup/help flow. Do not duplicate command recipes here; use the installed `agent-device` help for current command shapes and platform limits.
An AI agent should not claim it heard VoiceOver, TalkBack, or VoiceView. Instead, use `agent-device` to inspect what the app exposes to platform accessibility. Check the accessibility-tree evidence for:
- Interactive controls expose useful labels, roles, enabled/disabled state, and selected/checked state
- The currently focused element is the expected remote target after each D-pad press
- Modal opening moves focus inside the modal and hides or de-prioritizes background controls
- Closing a modal restores focus to the invoking control or another predictable target
- Dynamic state changes expose updated labels/state or a live-region/announcement path
Escalate to manual screen-reader testing for the parts `agent-device` cannot verify: exact spoken order, announcement timing, audio-description behavior, caption rendering, and platform-specific verbosity.
## Accessible Modals
```jsx
<Modal
visible={isVisible}
accessibilityViewIsModal={true}
onRequestClose={handleClose}
>
<View>
{/* Don't wrap the body in `accessible` — it collapses children into one
element and hides the buttons from focus. Label a header node instead. */}
<Text accessibilityRole="header">Continue watching?</Text>
<Text>Are you still watching?</Text>
<Button title="Yes" onPress={handleContinue} />
<Button title="No" onPress={handleExit} />
</View>
</Modal>
```
- Set initial focus inside modal
- Trap focus within modal
- Announce dialog opening
## Autoplay Content
- Include remote-focusable playback controls
- `AccessibilityInfo.isScreenReaderEnabled()` returns a **Promise** — `await` it (or subscribe to the `screenReaderChanged` event) before deciding whether to mute; a bare synchronous `if` is always truthy:
```jsx
const srOn = await AccessibilityInfo.isScreenReaderEnabled();
if (srOn) muteAutoplay();
```
- Apple TV: respect system audio focus, integrate with `AVAudioSession`
## Cross-Platform Focus
Prefer `TVFocusGuideView` over `nextFocus*` for shared layouts. `destinations` takes an array of resolved components (`ref.current`), not the ref objects and not string IDs. Don't build it inline — `ref.current` is `null` on the first render and mutating a ref triggers no re-render, so the guide would register nothing. Hoist the resolved components into state once the children have mounted:
```jsx
const playRef = useRef(null);
const infoRef = useRef(null);
const [destinations, setDestinations] = useState([]);
// Children assign their refs after this component first renders; push the
// resolved nodes into state so a NEW array is passed and the guide updates.
useEffect(() => {
setDestinations([playRef.current, infoRef.current].filter(Boolean));
}, []);
<TVFocusGuideView destinations={destinations}>
<TouchableOpacity ref={playRef} accessibilityRole="button" accessibilityLabel="Play" />
<TouchableOpacity ref={infoRef} accessibilityRole="button" accessibilityLabel="Info" />
</TVFocusGuideView>
```
See [focus-management.md](./focus-management.md) for when `nextFocus*` overrides are acceptable.
## Related Skills
- [a11y-overview.md](./a11y-overview.md) — Accessibility fundamentals for TV
- [a11y-checklist.md](./a11y-checklist.md) — Pre-launch checklist
- [focus-management.md](./focus-management.md) — Focus APIs
references/a11y-overview.md
---
title: Accessibility on TV Overview
impact: MEDIUM
tags: accessibility, a11y, screen-readers, focus, d-pad, talkback, voiceover
---
# Accessibility on TV — Overview
What's different about accessibility on TV versus mobile/web — the deltas you can't infer from general React Native a11y knowledge. For prop usage and patterns see [a11y-implementation.md](./a11y-implementation.md); for the audit list see [a11y-checklist.md](./a11y-checklist.md). WCAG/POUR and the legal baseline (ADA, Section 508, EN 301 549) apply to TV exactly as to mobile/web — treat a11y as a requirement, not a nice-to-have.
## Quick Reference
- TV accessibility differs from mobile/web on three axes: D-pad navigation, focus-driven UI, and viewing distance
- Focus management *is* the primary a11y surface on TV — there is no touch fallback
- Screen readers: TalkBack (Android TV), VoiceOver (Apple TV), VoiceView (Fire TV)
- Behavior is NOT consistent across platforms — test on each device, not just one
## Why TV Accessibility Is Unique
| Feature | TV | Mobile | Web |
|---------|-----|--------|-----|
| Input | Remote / voice | Touch + assistive tech | Keyboard, mouse, screen readers |
| Focus Management | Essential (D-pad) | Implicit with touch | Tab focus |
| Screen Reader | Limited/inconsistent | VoiceOver, TalkBack | JAWS, NVDA, VoiceOver |
| Text Scaling | Limited; fixed layout | Dynamic type | CSS zoom |
| Contrast | Critical (viewing distance) | Important | Important |
| Gestures | Not applicable | Swipe, pinch | Keyboard, mouse |
## Platform Screen Readers
| Platform | Tool | Activation |
|----------|------|------------|
| Fire TV | VoiceView | Accessibility settings |
| Apple TV | VoiceOver | Menu + Siri Remote, or Settings |
| Android TV | TalkBack | Accessibility shortcut or dev settings |
### Platform Differences
- **Fire TV (VoiceView):** Similar to TalkBack but not all props honored on older models. Extra verbose by default.
- **Apple TV (VoiceOver):** Focus can jump non-linearly. `accessibilityHint` read immediately after label. Role mapping consistent with iOS.
- **Android TV (TalkBack):** Linear D-pad navigation. `accessibilityHint` sometimes skipped unless `accessible={true}` is explicit.
These are the core React Native accessibility props (`accessible`, `accessibilityLabel`, `accessibilityRole`, `accessibilityState`, `accessibilityLiveRegion`, `accessibilityViewIsModal`) you'll use on TV, but support varies by platform — see [a11y-implementation.md](./a11y-implementation.md) for TV-specific usage and the platform quirks above for where they diverge.
## Related Skills
- [a11y-implementation.md](./a11y-implementation.md) — Detailed implementation guide
- [a11y-checklist.md](./a11y-checklist.md) — Pre-launch checklist
- [design-color.md](./design-color.md) — Contrast requirements
references/design-10foot.md
---
title: The 10-Foot Experience
impact: HIGH
tags: 10-foot, tv-design, remote, feedback, shared-screen
---
# The 10-Foot Experience
TV UIs are viewed from ~3 m (10 ft) with a D-pad remote, not 30 cm with a touchscreen. This file covers the TV-specific design concerns that follow from that distance and input model. For the concrete numbers, defer to the focused references:
- Text sizing for distance → [design-typography.md](./design-typography.md)
- Safe zones, spacing, grids → [design-layout.md](./design-layout.md)
- Contrast and color on TV panels → [design-color.md](./design-color.md)
- Focus movement and modal focus traps → [focus-management.md](./focus-management.md)
- Directional navigation rules → [nav-directional.md](./nav-directional.md)
## Quick Reference
- Design for legibility at 3 m; verify by testing from a couch with a remote, not at a desk with a keyboard.
- Acknowledge every remote press with a visual cue within ~100 ms.
- Keep focus/transition animations under 200 ms so they never delay the next input.
- Treat the TV as a shared device — don't expose personal data by default.
## Input Latency and Feedback
TV hardware and remotes add ~100–200 ms between a button press and the on-screen response. Mask that latency, don't add to it:
- Show a focus cue (highlight, ~3–5% scale, or border) within ~100 ms of every press.
- Keep focus and transition animations under 200 ms; an animation that outlasts the next press is too long.
- Use motion only to reinforce direction or highlight focus changes — never decorative sequences that block input.
## Shared Screen Considerations
TVs are shared among family, roommates, and guests — design for that:
- Don't display account email, payment details, or other personal data unless the action requires it.
- Make profile switching reachable within 2 D-pad presses from the home screen.
- Show a confirmation step for voice-entered text before submitting it.
- Auto-dismiss subtitles, overlays, and menus on a timer once they've served their purpose.
## Couch Test Checklist
Test each completed screen from the intended viewing position (couch, remote, ~3 m). Each step is pass/fail:
1. Reach the first **Play** action from the home screen in ≤3 D-pad presses.
2. Return to the starting screen using only the **Back** button.
3. After every directional press, focus lands on a visible element (no off-screen or lost focus).
4. Every press produces a visible cue within ~100 ms.
5. Screen remains legible with room lights both on and off.
If a step fails, reduce the number of focusable elements or realign the layout before adding manual `nextFocus*` overrides.
## Related Skills
- [design-layout.md](./design-layout.md) — Layout patterns, safe zones, component design
- [design-typography.md](./design-typography.md) — Text sizing for distance
- [design-color.md](./design-color.md) — Contrast and color for TV displays
- [focus-management.md](./focus-management.md) — Focus engine, traps, restoration
references/design-color.md
---
title: Color and Contrast for TV Displays
impact: MEDIUM
tags: color, contrast, hdr, palette, accessibility, tv-design
---
# Color and Contrast for TV Displays
Color systems that work on phones can wash out or over-glow on TV. Distance, panel technology, ambient light, and HDR all affect how colors appear.
## Quick Reference
- Aim for ≥4.5:1 contrast ratio for normal text, ≥7:1 for core UI
- Avoid pure white (#FFFFFF) on pure black (#000000) for entire screens — causes eye fatigue
- Focus indicators need multi-cue: color + border/outline + mild scale
- Test in both SDR and HDR modes; ship a palette that works in both
## Contrast Ratios
| Context | Minimum Ratio | Notes |
|---------|--------------|-------|
| Normal text | ≥ 4.5:1 | WCAG standard |
| Core UI (menus, buttons, captions) | ≥ 7:1 | Holds up on cheap panels and bright rooms |
| Focus indicators | Multi-cue | Color alone fails for color-deficient users |
## Color Palette with Contrast Ratios
| Purpose | Color | vs Black | vs Dark Gray (#1a1a1a) | Use Case |
|---------|-------|----------|----------------------|----------|
| Primary text | #FFFFFF | 21:1 | 17.8:1 | Body text, headings |
| Secondary text | #E5E5E7 | 18.5:1 | 15.7:1 | Labels, metadata |
| Tertiary text | #A1A1AA | 8.6:1 | 7.3:1 | Timestamps, auxiliary |
| Disabled text | #6B7280 | 4.2:1 | 3.6:1 | Inactive elements (sparingly) |
| Primary accent | #007AFF | 8.2:1 | 7.0:1 | Links, CTAs, selected states |
| Focus border | #00D4FF | 11.3:1 | 9.6:1 | Focus indicators, active |
| Success | #34C759 | 9.8:1 | 8.3:1 | Confirmations, positive |
| Warning | #FF9500 | 7.1:1 | 6.0:1 | Warnings, notices |
| Danger | #FF3B30 | 5.9:1 | 5.0:1 | Errors, destructive actions |
> WCAG 1.4.3 requires **4.5:1** for normal text and **3:1** for large text (≥24px, or ≥18.66px bold). The disabled-text row falls below 4.5:1 by design — inactive/disabled UI components are exempt from the contrast minimum. Don't reuse that ratio for active text.
## Ambient Light Adaptation
| Environment | Background | Primary Text | Secondary Text | Accent | Min Contrast |
|-------------|-----------|-------------|----------------|--------|-------------|
| Bright room | #000000 | #FFFFFF | #E5E5E7 | #007AFF | 7:1 |
| Dim room | #1a1a1a | #E5E5E7 | #B3B3B3 | #5AC8FA | 4.5:1 |
| Dark room | #2a2a2a | #D1D1D6 | #8E8E93 | #64D2FF | 3:1 |
> The dark-room 3:1 minimum applies to **large text and UI components** only (WCAG large-text / non-text contrast). Keep body text at 4.5:1 regardless of ambient profile.
## HDR Considerations
HDR displays show brighter whites and deeper blacks — great for video, but UI can over-glow:
- Keep UI whites around **80-90% luminance** — avoid hard #FFFFFF for long-lived text
- Watch highlights (focus glow, selection chips) against HDR content — cap intensity to prevent blooming
- Test both SDR and HDR modes; ship a palette that doesn't collapse in either
## Display Variations
Consider various TV display technologies:
- **LCD, LED, QLED, OLED** render colors differently
- Design for **Standard picture mode** — many TVs offer Cinema/Game/HDR modes
- Use **sRGB color space** for consistency across TVs and mobile
- Darker colors save power on OLED screens
## Technical Considerations
- **Gradients:** Can display as color bands — use high-color-depth gradients
- **Dithering:** Apply noise to reduce color banding — creates illusion of more colors
- **Color-based information:** Never rely solely on color to convey information — add text labels
- **Test on multiple devices** and color spaces — viewing in real life matters
## Related Skills
- [design-typography.md](./design-typography.md) — Text sizing and readability
- [design-10foot.md](./design-10foot.md) — 10-foot experience principles
- [a11y-implementation.md](./a11y-implementation.md) — Accessible color and contrast
references/design-layout.md
---
title: Layout Patterns and Common Components
impact: HIGH
tags: layout, cards, swimlanes, safe-zones, overscan, responsive, tv-design
---
# Layout Patterns and Common Components
## Quick Reference
- Use safe zones (5-10% margin) to prevent overscan clipping
- Leave enough spacing for focus indicators to scale or glow without overlapping adjacent cards
- Keep row-to-row and card-to-card focus movement visually predictable
- Test layouts in SDR/HDR and common TV display modes because overscan and color processing vary
## Cards and Content Tiles
- Focus feedback: 3-5% scale increase, glow/drop shadow for depth
- Leave spacing so focus indicators never overlap adjacent cards
- Avoid cramming too much info; dense cards are harder to scan from TV distance
## Rows / Swimlanes
- Left/right within a row, up/down between rows
- Auto-scroll when focus reaches row edge
- Keep row headers readable and announced as landmarks where appropriate
- Keep fewer, more distinct categories rather than many nearly identical rows
## TV Scroll Alignment
On `react-native-tvos`, `ScrollView` has TV-only props for focus-driven snapping:
- Use `snapToAlignment="item"` when each child needs its own snap alignment through `scrollSnapAlign`.
- Use `scrollSnapOffset` when different rows/items need different landing offsets.
- Use `scrollAnimationEnabled={false}` when animated focus scrolling adds input latency or causes overshoot.
Do not combine TV snapping modes with paging assumptions without testing D-pad focus movement; two scroll-positioning systems can fight each other.
## Buttons
- Focused buttons: contrast, outlines, or subtle scaling
- Group related actions ("Play" + "More Info") with consistent spacing
- Short verb labels: "Play", "Retry", "Cancel"
## Overlays
Video controls, pause menus:
- Fade in quickly, fade out after inactivity
- Predictable focus order (left to right)
- Dim content beneath but don't hide completely
## Safe Zones
Many TVs apply overscan — outer 5-10% may get cropped:
- Keep all essential elements (text, logos, buttons) inside 5-10% margin
- Backgrounds and hero images can extend to the edge
- Use gridlines or bounding boxes during development to visualize safe boundaries
## Responsive TV Design
TVs range from 32" to 85" and don't all render pixels identically:
- **Use relative units** (viewport height/width, percentages) not fixed pixels
- **Center critical content** — peripheral areas less reliable
- **Test multiple display modes:** Standard, Cinema, Game, HDR
- Design around 16:9 base grid, ensure it adapts to 21:9 without breaking
## Spacing
- Consistent vertical rhythm between rows (1.5x card height for padding)
- Invisible baselines for text/components keep focus transitions smooth
- TV design prioritizes clarity over space efficiency
## Related Skills
- [design-10foot.md](./design-10foot.md) — 10-foot experience principles
- [design-typography.md](./design-typography.md) — Text sizing and readability
- [perf-lists.md](./perf-lists.md) — List virtualization for performance
references/design-typography.md
---
title: Typography for TV Displays
impact: HIGH
tags: typography, fonts, text-size, readability, contrast, tv-design
---
# Typography for TV Displays
Typography that works on phones fails on TV. Distance, panel tech, ambient light, and OS rendering push designs toward bigger type, clearer spacing, and higher contrast.
## Quick Reference
- Start TV body text around 24px and validate from viewing distance
- Avoid ultra-light/ultra-thin weights — they shimmer on LCDs, bloom on OLEDs
## Minimum Font Sizes
Use these as starting points, then validate on the target TV size and distance:
| Text Style | TV Starting Point | Use Case |
|-----------|-------------------|----------|
| Body | 24px | Descriptions, paragraphs |
| Caption | 20px | Metadata, labels, tags |
| Button | 22px | Interactive elements, CTAs |
| Heading | 32px | Section titles, categories |
| Title | 48px | Page titles, hero text |
| Display | 64px | Large promotional text |
```jsx
const tvTypography = StyleSheet.create({
body: { fontSize: 24, lineHeight: 32, fontWeight: '400' },
button: { fontSize: 22, lineHeight: 28, fontWeight: '600' },
caption: { fontSize: 20, lineHeight: 26, fontWeight: '400' },
heading: { fontSize: 32, lineHeight: 40, fontWeight: '600' },
title: { fontSize: 48, lineHeight: 56, fontWeight: '700' },
});
```
## Line Spacing and Letter Spacing
| Context | Line Height | Letter Spacing | Best For |
|---------|-------------|----------------|----------|
| Content (paragraphs) | 1.4x | 0.4px | Long-form reading |
| Navigation (menus) | 1.2x | 0.5px | Menu items, scanning |
| Display (large text) | 1.15x | -0.4px to -0.8px | Hero text, titles |
Negative tracking for large titles: huge sizes amplify default spacing; tightening avoids airy gaps.
## Text Rendering on TV
TVs expose edge cases with subpixel rendering:
- **Text over images:** Add subtle text shadow to separate from backgrounds
```jsx
const readableOnImage = {
color: '#FFF',
textShadowColor: 'rgba(0,0,0,0.35)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
};
```
- **Subtitles/over-video:** Combine shadow + stroke at low opacity (not heavy blur)
- **Safe zone:** Keep text inside safe zone — overscan clips labels at edges
- **Tile titles:** `numberOfLines={2}` + `ellipsizeMode="tail"` — avoid wrapping issues
- **Long localized titles:** Gentle marquee only on focus, never by default
## Related Skills
- [design-10foot.md](./design-10foot.md) — 10-foot experience design principles
- [design-color.md](./design-color.md) — Color and contrast guidelines
- [design-layout.md](./design-layout.md) — Layout and spacing patterns
references/focus-management.md
---
title: Focus Management
impact: CRITICAL
tags: focus, tvfocusguideview, hastvpreferredfocus, d-pad, focus-traps, tv
---
# 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.
```jsx
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 `Component`s (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:
```jsx
<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:
```jsx
<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
```jsx
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
```jsx
<Pressable
testID="playButton"
style={({ focused }) => ({
borderWidth: focused ? 2 : 0,
borderColor: focused ? 'red' : 'transparent',
})}
>
<Text>Play</Text>
</Pressable>
```
### Add Logs
```jsx
<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
- [focus-performance.md](./focus-performance.md) — Performance impact of focus changes
- [nav-directional.md](./nav-directional.md) — Directional navigation fundamentals
- [nav-patterns.md](./nav-patterns.md) — Navigation patterns and focus restoration
references/focus-performance.md
---
title: Focus Performance
impact: CRITICAL
tags: focus, performance, re-renders, react-memo, transforms, tv
---
# Focus Performance
On TV, every D-pad press sends focus change events. Careless handling triggers dozens of component updates per press, tanking your frame rate.
## Quick Reference
- Keep focus effects local — don't update global state for visual changes
- Prefer a single top-level focus frame over per-card overlays
- Use transforms (scale, translate) not layout properties (width, height) for focus animations
- Batch focus updates in one render frame
## Why It's Worse on TV
- **Focus cascades:** Changing focus in one row can cause style updates in multiple rows
- **Platform quirks:** Tizen/webOS trigger both onBlur and onFocus for multiple elements in quick succession
- **Remote latency:** Bluetooth/IR remotes already have inherent latency — any JS thread delay makes it worse
- Users hold directions or rapidly press buttons — your app must process multiple focus events per second
## Problem: Cascading Renders
**Bad** — Every card stores its own focus state in React state:
```jsx
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
if (focusedId === id) setIsFocused(true);
else setIsFocused(false);
}, [focusedId]);
```
Every change to `focusedId` re-renders every card in the row.
**Better** — Memoize the card:
```jsx
const Card = React.memo(({ isFocused, poster }) => (
<Image
style={isFocused ? styles.focused : styles.normal}
source={poster}
/>
));
```
Only the focused card re-renders, not the whole row.
## Problem: Overlay Flicker
**Bad** — Unmounting/remounting overlays on focus:
```jsx
{isFocused && <FocusOverlay />}
```
**Better** — Toggle opacity:
```jsx
<View style={{ opacity: isFocused ? 1 : 0 }}>
<FocusOverlay />
</View>
```
Avoids flicker but mounts dozens of hidden overlays consuming memory.
**Best** — Single top-level focus frame:
```jsx
const [frame, setFrame] = useState(null);
const onCardFocus = (ref) => {
ref.current?.measure((x, y, w, h, pageX, pageY) => {
setFrame({ x: pageX, y: pageY, w, h });
});
};
return (
<View style={{ flex: 1 }}>
<FlatList data={movies} renderItem={({ item }) => {
const ref = useRef(null);
return (
<Card ref={ref} item={item} onFocus={() => onCardFocus(ref)} />
);
}} />
{frame && (
<Animated.View style={[styles.absolute, {
left: frame.x, top: frame.y,
width: frame.w, height: frame.h
}]} />
)}
</View>
);
```
One focus frame moves around — no duplication, minimal re-renders.
## TV-Specific Checks
1. **Keep focus effects local** — Don't update Redux/Zustand for visual focus changes.
2. **Preload focus styles** — Shadows, glows, gradients are GPU-expensive to generate on the fly. Pre-render them and toggle visibility.
3. **Batch focus updates** — A single D-pad press can fire blur and focus events across multiple elements.
4. **Avoid layout shifts** — Changing focus geometry can make the next directional search unstable.
5. **Use platform-specific focus helpers:**
- On Android TV/Fire TV: `focusable` prop reduces extra focus jumps
- On Apple TV/Android TV/Fire TV: `TVFocusGuideView` to control focus without excessive JS
## Related Skills
- [focus-management.md](./focus-management.md) — Core focus APIs and debugging
- [perf-animations.md](./perf-animations.md) — Animation performance on TV
- [perf-overview.md](./perf-overview.md) — Overall TV performance strategy
references/nav-directional.md
---
title: Directional Navigation Fundamentals
impact: CRITICAL
tags: navigation, directional, focus-engine, d-pad, spatial-navigation, tv
---
# Directional Navigation Fundamentals
Every TV app starts with a simple question: where does the focus go next? When a viewer presses an arrow on the remote, the app must decide which element becomes active.
## Quick Reference
- TV navigation is fundamentally physical — each button press is deliberate
- Platform focus engines differ: tvOS uses spatial inference, Android TV uses proximity, web-based TVs need JS libraries
- Design layouts that work with the focus engine, not against it
- Align and space elements logically so directional presses resolve to the intended neighbor
## How Focus Engines Work
### tvOS — High Precision
Apple's engine examines layout geometry:
- Searches for focusable views based on spatial proximity in the pressed direction
- Treats related items as "focus islands" (cohesion zones)
- Expects clean grid/alignment patterns
- When layouts are clean: focus slides and decelerates like real physics
- Account for diagonal movement and inertia-based swipes
### Android TV — Developer-Defined
More flexible, leans on developer direction:
- Focus moves to nearest visible item along pressed direction (Cartesian)
- Override with `nextFocusUp`, `nextFocusDown`, `nextFocusLeft`, `nextFocusRight`
- Tolerates less regular layouts
- When no valid target exists, focus can disappear
### Vega OS
Same Cartesian focus management as Android TV — focus moves to "closest" item in D-pad direction.
### Web-Based TVs (Tizen, webOS)
No native focus engine — must use JavaScript spatial navigation:
- `@noriginmedia/norigin-spatial-navigation` is the most popular library
- Keeps a registry of focusable nodes
- Listens for arrow/enter keys
- Decides next target based on geometry and direction
## Preferred Approach: Let the Platform Lead
For most apps, the best focus management is no explicit management at all:
```jsx
<View style={styles.row}>
{items.map((item) => (
<Pressable
key={item.id}
onPress={() => select(item)}
onFocus={() => setFocusedItem(item.id)}
>
<Image source={item.poster} />
</Pressable>
))}
</View>
```
**Key strategies:**
- Align and space elements logically
- Avoid dead zones — gaps cause unpredictable jumps
- Group related UI into containers ("focus islands")
- On tvOS: account for diagonal movement; on Android TV: design for strict up/down/left/right
If focus suddenly behaves strangely, it's usually a sign the layout needs adjusting, not that you need more code.
## React Native TV's Focus Tree
Each platform builds a focus tree — an internal map of all focusable elements. Every `Pressable`, `Touchable`, or `TextInput` becomes a node. React Native TV mirrors this with unified focus APIs.
## Two Navigation Layers
1. **Global navigation** — Moves between main sections (Home, Search, Settings). Typically a drawer.
2. **Local navigation** — Operates within a section (Popular, Recommended tabs). Typically tabs.
## Building Predictable Navigation
- **Consistent movement:** If "right" moves to next card, keep that everywhere
- **Let layout lead:** Clear alignment helps the engine make right decisions
- **Plan focus transitions:** Define where focus starts, how "back" behaves, what regains focus on return
- **Shallow hierarchy:** Too many layers makes users lose their bearings
## Related Skills
- [focus-management.md](./focus-management.md) — TVFocusGuideView, hasTVPreferredFocus, debugging
- [nav-patterns.md](./nav-patterns.md) — Drawer, tabs, modals, back navigation
- [design-layout.md](./design-layout.md) — Layout patterns that support natural focus flow
references/nav-keyboard.md
---
title: Keyboard Handling
impact: HIGH
tags: keyboard, text-input, voice-input, remote, tvevent, tv
---
# Keyboard Handling
TV remotes were never meant for typing. Each character takes several arrow presses and a click. Minimize typing and make keyboards work well when needed.
## Quick Reference
- **Rule #1: Minimize input** — Use pre-filled options, voice input, search history, auto-complete
- System keyboards feel natural to users; customize them before building custom
- Map RCU buttons (play/pause) to confirm/cancel actions
- Consider companion apps and QR code auth to eliminate typing entirely
## Input Minimization Strategies
1. **Pre-filled options** — Past searches, popular searches, or both
2. **Voice input** — React Native Voice library
3. **Real-time validation** — Minimize correction needs
4. **Input history** — Let users reuse previous searches
5. **Mobile companion apps** — For authentication, casting, second screen
6. **QR code authentication** — TV displays QR, phone scans it
## Built-In System Keyboards
Trigger the default keyboard with a standard `TextInput`. System keyboards differ by platform but feel natural to users.
### Android TV (GBoard for TV)
Grid-based keyboard, navigate with arrow keys, confirm with "OK" button.
### Apple tvOS
Row-based keyboard, scroll with remote swipe gestures. Supports dictation and iOS Remote app.
### Keyboard Types
Prefer the narrowest `keyboardType` (`numeric`/`number-pad` for PINs) — it swaps the full grid keyboard for a smaller one, cutting D-pad travel. Use `secureTextEntry` for passwords (not a keyboardType).
## Customizing the Built-In Keyboard
Use `useTVEventHandler` to map RCU buttons to actions:
```jsx
import { useTVEventHandler } from 'react-native';
const SearchScreen = () => {
const inputRef = useRef(null);
const inputValueRef = useRef('');
const handleSearch = () => {
console.log('Search submitted:', inputValueRef.current);
};
useTVEventHandler((evt) => {
if (evt.eventType === 'play') {
handleSearch();
}
});
return (
<TextInput
ref={inputRef}
placeholder="Search TV shows..."
onChangeText={(text) => { inputValueRef.current = text; }}
onSubmitEditing={handleSearch}
/>
);
};
```
## Custom Keyboards
When the default keyboard is insufficient (YouTube-style search), build your own:
```jsx
const [showKeyboard, setShowKeyboard] = useState(false);
<KeyboardAvoidingView behavior="position" style={{ flex: 1 }}>
<TextInput
onFocus={() => setShowKeyboard(true)}
showSoftInputOnFocus={false}
/>
{showKeyboard && <CustomKeyboard onKeyPress={handleKeyPress} />}
</KeyboardAvoidingView>
```
**Key considerations:**
- Set `showSoftInputOnFocus={false}` to prevent system keyboard
- Handle two states: focused and selected for each key
- Make discoverable — users should know how to use auto-complete without guessing
## Voice Input
- **System keyboard approach:** Rely on system voice dictation (simplest, most reliable)
- **Custom keyboard:** Write native modules or use `react-native-voice`
- **Important:** Adding voice input requires microphone and speech recognition permissions
## Mobile Companion Apps
Enable communication between mobile and TV apps for:
- Authentication (easiest: QR code scan)
- Media casting
- Second screen experiences (stats, chats, polls)
- Text input from phone keyboard
Communication via local network (Wi-Fi) for media streaming scenarios.
## Related Skills
- [nav-patterns.md](./nav-patterns.md) — Overall navigation structure
- [a11y-implementation.md](./a11y-implementation.md) — Accessible input handling
references/nav-patterns.md
---
title: Navigation Patterns
impact: CRITICAL
tags: navigation, drawer, tabs, modals, back-navigation, focus-restoration, tv
---
# Navigation Patterns
TV navigation uses two layers: global navigation (between sections) and local navigation (within sections). The goal is predictable navigation — users should reach content with minimal button presses and no confusion.
## Quick Reference
- Use drawer for global navigation, tabs for local navigation
- Always restore focus when returning from modals/overlays
- Keep the back button behavior consistent: each press = one layer back
- Trap focus inside modals and overlays until dismissed
## Drawer Navigation (Global)
The main menu, typically on the left edge:
- Opens when user presses left from leftmost area (or menu/back button)
- Rest of screen dims slightly to signal context shift
- Focus is trapped inside until user exits or selects
```jsx
<Drawer isOpen={open}>
<MenuItem label="Home" onPress={() => navigate('home')} />
<MenuItem label="Movies" onPress={() => navigate('movies')} />
<MenuItem label="Settings" onPress={() => navigate('settings')} />
</Drawer>
```
**Best practices:**
- Limit to 5-7 items
- Use clear labels (icons + text)
- Restore focus to previously active element when drawer closes
- Transitions under 200ms — should feel like infrastructure, not a feature
## Tab Navigation (Local)
Organizes content within a single section:
- Typically beneath hero banner or above first row
- 3-5 tabs maximum
- Left/right to switch tabs, down to enter content rows
```jsx
<Tabs>
<Tab label="Popular" onFocus={() => setCategory('popular')} />
<Tab label="New" onFocus={() => setCategory('new')} />
<Tab label="Favorites" onFocus={() => setCategory('favorites')} />
</Tabs>
```
Horizontal tabs as primary navigation work for simple apps. Complex apps benefit from drawer-based approach.
## Modal Navigation
Modals are temporary, focused interruptions:
```jsx
<Modal visible={showDetails}>
<Text>Are you sure you want to remove this item?</Text>
<Button label="Cancel" onPress={() => setShowDetails(false)} />
<Button label="Confirm" onPress={handleConfirm} />
</Modal>
```
**Guidelines:**
- Trap focus inside — dim/blur background
- Transitions ~150ms
- Never stack multiple modals
- Consistent placement (center fade/scale typically works)
- When modal closes, restore focus to element that triggered it
## Back Navigation & Focus Restoration
When users press back, they expect:
1. Return to the same screen
2. Focus on the element they were using before
### Remembering Last Focused Element
`TVFocusGuideView` manages this internally — each guide maintains the last element that held focus. When user returns, the same element is refocused.
```jsx
function ConfirmModal({ visible, onClose, returnRef }) {
return visible ? (
<TVFocusGuideView trapFocusUp trapFocusDown>
<Pressable hasTVPreferredFocus onPress={onClose}>
Confirm
</Pressable>
<Pressable onPress={() => {
onClose();
returnRef?.current?.focus();
}}>
Cancel
</Pressable>
</TVFocusGuideView>
) : null;
}
```
### Keeping Back Flow Consistent
Each back press should move back one layer and restore previous focus state. This sequence must be the same everywhere in your app.
## Navigation Predictability
- Always provide a visible focus state
- Never move focus off-screen without scrolling into view
- Keep focusable elements reasonable — users shouldn't press buttons excessively
- Use consistent directional logic: if left opens drawer on one screen, it should do the same on all screens
## Implementation with React Navigation
### Drawer
```jsx
const Drawer = createDrawerNavigator();
<Drawer.Navigator screenOptions={{
drawerType: 'permanent',
drawerStyle: { width: 240 },
}}>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Movies" component={MoviesScreen} />
</Drawer.Navigator>
```
### Tabs
```jsx
const Tab = createBottomTabNavigator();
<Tab.Navigator screenOptions={{
tabBarStyle: { height: 80 },
tabBarLabelStyle: { fontSize: 18 },
}}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Movies" component={MoviesScreen} />
</Tab.Navigator>
```
## Related Skills
- [focus-management.md](./focus-management.md) — TVFocusGuideView, focus traps
- [nav-directional.md](./nav-directional.md) — How focus engines work
- [nav-keyboard.md](./nav-keyboard.md) — Keyboard handling on TV
references/perf-animations.md
---
title: Animation Performance on TV
impact: CRITICAL
tags: animations, reanimated, native-driver, transforms, focus, tv
---
# Animation Performance on TV
Animations make a TV app feel polished — if they're smooth. On TV hardware, JS-driven animations tank performance fast because the JS thread competes with focus handling, list rendering, and playback controls.
## Quick Reference
- Keep focus animations short: 100-150ms
- Keep focus animations off the JS thread; JS also handles remote input and player controls
- Avoid focus animations that change layout or move adjacent focus targets
- Test with the actual remote — keyboard/dev tools hide input lag
## Why It's Worse on TV
- **Tight CPU budgets:** Fire TV Stick Lite JS thread runs ~200-300 MHz while decoding 4K
- **Remote input expectations:** Unlike touch, TV navigation feels broken if animations delay focus
- **Extra compositor hops:** Tizen/webOS have more rendering pipeline layers
## Focus Scale Animation
**Bad (JS thread):**
```jsx
const [scale, setScale] = useState(1);
useEffect(() => {
if (isFocused) setScale(1.1);
else setScale(1);
}, [isFocused]);
<View style={{ transform: [{ scale }] }} />
```
**Better (native-driven):**
```jsx
const scale = useRef(new Animated.Value(1)).current;
useEffect(() => {
Animated.spring(scale, {
toValue: isFocused ? 1.1 : 1,
useNativeDriver: true,
}).start();
}, [isFocused]);
<Animated.View style={{ transform: [{ scale }] }} />
```
Runs entirely on UI thread — JS is free for input and logic.
## Chained Animations
JS-driven chains (fade → scale → shadow) cause multiple layout passes.
**Better:**
- Combine into one `Animated.parallel` call, all using native driver
- Or use **Reanimated 3** to orchestrate in a single worklet off JS thread
## TV-Specific Checks
1. **Keep animations short:**
- 100-150ms for focus changes = feels instant
- 300-500ms "hero" animations only for big transitions
2. **Do not move focus targets during focus search** — Layout-changing focus effects can make the next D-pad direction ambiguous.
3. **Keep complex sequences off JS** — Home-screen heroes, auto-scrolling carousels, and parallax must not block remote input.
4. **Test with actual remote** — Keyboard and dev tools hide input lag. Even 50ms extra delay is noticeable on a remote.
## Platform Quirks
- **Apple TV:** Native focus engine provides subtle scaling — avoid doubling unless you disable defaults
- **Tizen:** Focus/blur events can be delayed by OS — match animation durations to platform responsiveness
- **Fire TV:** Aggressive frame skipping if animations aren't native-driven; can drop to 30 FPS instantly
> When in doubt, animate less. On TV, a fast and crisp focus change beats a slow, fancy effect every time.
## Related Skills
- [focus-performance.md](./focus-performance.md) — Focus-specific render optimization
- [perf-overview.md](./perf-overview.md) — Overall performance strategy
- [perf-lists.md](./perf-lists.md) — List scrolling performance
references/perf-lists.md
---
title: "Lists and Grids: Virtualization Is Mandatory"
impact: CRITICAL
tags: lists, grids, virtualization, flashlist, flatlist, tv
---
# Lists and Grids: Virtualization Is Mandatory
TV UIs are grids of lists inside lists. Home screens have 10-15 rows with 10-20 items each. Without virtualization, your app will be unusable on TV hardware.
## Quick Reference
- **Always virtualize large feeds** — Use FlatList/VirtualizedList, FlashList, or RecyclerListView instead of mounting every poster
- Keep poster rows lightweight; heavy shadows/gradients compound across dozens of focused cards
- Preload only the next likely row/screen; aggressive poster prefetch can trigger TV memory kills
- On `react-native-tvos`, use `additionalRenderRegions` for critical ranges that must stay mounted during focus navigation
- Render hero row outside the virtualized list
## Why It's Worse on TV
- **Lower RAM:** Fewer off-screen items can stay in memory
- **No GPU tile caching:** On older Tizen/webOS, scrolling back = re-render from scratch
- **Focus-driven navigation:** Users whip through rows faster than mobile swipes
- **Large assets:** Movie posters, 4K stills are heavier than mobile thumbnails
## Bad: Non-Virtualized Grid
```jsx
<ScrollView>
{rows.map((row) => (
<Row key={row.id} data={row.items} />
))}
</ScrollView>
```
Every item in every row exists in memory all the time.
## Better: Virtualized with FlashList
```jsx
<FlashList
data={movies}
renderItem={renderPoster}
/>
```
Only a "window" of items exists in memory at any time.
## React Native TV VirtualizedList
`react-native-tvos` wraps `VirtualizedList` contents with TV focus helpers and adds `additionalRenderRegions`:
```jsx
<FlatList
data={rows}
renderItem={renderRow}
additionalRenderRegions={[{ first: 0, last: 1 }]}
/>
```
Use `additionalRenderRegions` sparingly for critical ranges that must not blank out during D-pad navigation, such as the current row plus an adjacent row. These regions are still a memory tradeoff.
## TV-Specific Checks
1. **Virtualize nested rows** — TV home screens often have many horizontal rows inside a vertical feed. Avoid keeping every poster mounted.
2. **Preload conservatively** — Prefetch images for the next likely screen or row, then verify memory while video is mounted.
3. **Keep focus work local** — Moving focus across one row should not re-render unrelated rows.
4. **Measure fast remote repeats** — Users can hold a direction and traverse rows faster than mobile swipe assumptions.
5. **Defer rich metadata** — Load ratings, trailer previews, and entitlement badges after the row is visible or focused.
6. **Hero row outside list** — Render the hero row outside the virtualized list to avoid recalculating its layout during row scroll.
## Platform Quirks
- **Tizen:** Prefetching too aggressively triggers out-of-memory reloads. Keep cache conservative.
- **webOS:** Scrolling performance drops if images aren't decoded yet — preload 1-2 screens ahead.
- **Apple TV:** Generally smoothest rendering, but older models still choke on giant grids.
## Related Skills
- [perf-overview.md](./perf-overview.md) — Overall performance strategy
- [perf-memory.md](./perf-memory.md) — Image and memory optimization
- [design-layout.md](./design-layout.md) — Row/card layout patterns
references/perf-memory.md
---
title: Memory Management on TV
impact: HIGH
tags: memory, ram, image-optimization, garbage-collection, tv-performance
---
# Memory Management on TV
On TVs, you're sharing RAM with the OS, video decoder, DRM, audio buffers, and even the live TV tuner. Your UI runs in the leftovers.
## Quick Reference
- Many devices have 1-1.5 GB total — your app might only get 300-500 MB
- 4K video streams eat 100-200 MB just for decoded frames
- Poster/backdrop caches are the biggest UI-side memory lever
- Smart TVs aggressively reclaim memory from your app
## Symptoms of Memory Pressure
- Sudden GC spikes (frame drops during scrolling)
- Images unloading from cache and re-downloading mid-session
- Crashes or forced restarts (Tizen and webOS are notorious)
## Image Memory Optimization
**Bad:**
```jsx
<Image source={{ uri: posterUrl }} />
```
Without cache control, changing `posterUrl` holds multiple decoded bitmaps until GC runs.
**Better:**
```jsx
<Image
source={{ uri: posterUrl, cache: 'force-cache' }}
resizeMode="cover"
/>
```
Or use `react-native-fast-image` for cache control.
## List Item Memory
Even with virtualization, if row components keep large objects in state (full metadata blobs), you're holding memory hostage.
**Better:** Store only IDs in list item state, fetch full details on demand.
## TV-Specific Checks
1. **Match asset size to display size** — A decoded 4K backdrop for a small thumbnail wastes the same memory as a visible full-screen asset.
2. **Measure with video mounted** — UI memory that looks fine without playback can fail once decoded frames and DRM buffers exist.
3. **Keep cache pressure visible** — Watch for poster eviction/re-download loops during fast row navigation.
4. **Avoid large list item state** — Keep IDs in rows; fetch full metadata on demand.
5. **Use native profiling tools:**
- Android TV/Fire TV: Android Studio Profiler → Memory tab
- Apple TV: Xcode Instruments → Allocations + Leaks
- Tizen/webOS: Emulator memory usage overlays
## Platform Quirks
- **Low-end Fire TV:** ~0.5-1 GB RAM total. Every extra library adds startup time.
- **Tizen/webOS:** Aggressive OS memory reclaim — your app can be killed without warning.
- **Apple TV 4K:** More generous RAM (4 GB) but don't assume you can skip optimization.
## Related Skills
- [perf-overview.md](./perf-overview.md) — Overall performance strategy
- [perf-lists.md](./perf-lists.md) — Virtualized lists reduce memory
- [perf-network.md](./perf-network.md) — Caching and payload optimization
references/perf-network.md
---
title: Network Performance on TV
impact: HIGH
tags: network, prefetching, caching, optimistic-ui, payloads, tv
---
# Network Performance on TV
On TV, there's no "loading spinner safety net." People expect content to instantly fill the screen. If nothing happens after a remote press, they'll assume the app froze — and press again (duplicate requests).
## Quick Reference
- Show something within 200ms of navigation — even a blurred poster or placeholder
- Prefetch the next likely screen while current one is stable
- Never block navigation/focus on network responses
- Debounce input during network stalls to prevent duplicate requests
## Why It's Worse on TV
- **Wi-Fi is often bad:** TVs in far corners, running on 2.4 GHz with packet loss/jitter
- **Platform timeouts:** Tizen aggressively kills "stuck" requests
- **Large payloads:** Home screen = multiple JSON payloads + dozens of poster images
- **No background fetch:** TVs don't run your app in background between sessions
## Problem: Blocking Navigation
**Bad:**
```jsx
const onRowFocus = async (rowId) => {
const details = await fetchRowDetails(rowId); // Blocks focus
setDetails(details);
};
```
User presses down, nothing highlights until network returns.
**Better (optimistic UI):**
```jsx
const onRowFocus = (rowId) => {
highlightRow(rowId); // Instant visual feedback
fetchRowDetails(rowId).then(setDetails);
};
```
## TV-Specific Checks
1. **Prefetch where it matters** — Preload the next likely screen or row, then verify memory on low-end devices.
2. **Use placeholders instead of blocking** — Focus movement should remain instant even when row metadata is stale or still loading.
3. **Prioritize visible content** — Load the hero/current row first; defer secondary rows and rich metadata.
4. **Handle retries gracefully** — Remote presses during stalls should not spam duplicate requests.
5. **Keep caches memory-aware** — Cached posters plus JSON plus video buffers can create GC stutter on Fire TV and smart TVs.
## Platform Quirks
- **Tizen:** Requests >5 seconds can be killed without warning. Set shorter timeouts + retry logic.
- **webOS:** Some models cache aggressively in firmware — add version param to URLs.
- **Fire TV:** Prefetching too aggressively on slow Wi-Fi spikes memory (cached images + JSON) → GC stutter.
- **Apple TV:** Fast on wired Ethernet but test on Wi-Fi too.
> Optimize first paint time, not just throughput. Show something within 200ms of navigation.
## Related Skills
- [perf-overview.md](./perf-overview.md) — Overall performance strategy
- [perf-memory.md](./perf-memory.md) — Memory impact of caching
- [perf-lists.md](./perf-lists.md) — Virtualized list rendering
references/perf-overview.md
---
title: Performance Overview for TV
impact: HIGH
tags: performance, device-tiers, kpis, startup, hardware, tv
---
# Performance Overview for TV
TV hardware is significantly weaker than modern phones. A 65" TV is often closer to a budget Android phone in CPU/GPU terms — while also decoding 4K video.
## Quick Reference
- Set performance budgets from the weakest supported TV device
- Keep one low-end streaming stick or TV in the regular test matrix
- Measure input latency, FPS during focus movement, memory, startup, and time to playback
- Treat video playback as part of the performance budget; UI work competes with decode and buffers
## Why Performance Is Unforgiving on TV
- **CPU/GPU budgets are lower** — every unnecessary render competes with video decoding
- **Memory: 1-2 GB** — shared between OS, video buffer, and your app
- **Users notice everything** — 300ms input delay = "the app froze"; 45 FPS = visible stutter
- **TV chipsets are designed for video playback**, not high-performance UI rendering
## Device Tiers — Progressive Enhancement
### Low-End (Fire TV Stick Gen 1)
Keep it lean. Drop fancy gradients, heavy shadows, long transitions. Stick to snappy focus highlights, lightweight lists, instant feedback. Responsiveness beats visual flair.
### Mid-Range (Samsung Smart TV mid-tier)
Layer in some polish. Quick scale/fade here and there. Still performance-first.
### High-End (Apple TV 4K, Nvidia Shield)
Add visual polish: parallax banners, chained animations, cinematic transitions.
**Implementation:**
- Detect hardware class at runtime (device model, RAM, OS version)
- Maintain feature flags for performance tiers (basic, standard, enhanced)
- Shared baseline layout + conditional animations/effects per tier
- Test on real devices at each tier
## KPIs to Track
| Metric | Low-End Target | Mid-End | High-End |
|--------|---------------|---------|----------|
| Cold start time | <5s | <4s | <4s |
| Time to playback | <10s | <7s | <7s |
| Time to first meaningful paint | <3s | <2s | <1.5s |
| FPS during navigation | 60 | 60 | 60 |
## TV-Specific Performance Checks
1. **Remote input latency** — Measure the delay from D-pad press to visible focus movement. Keyboard input and simulator clicks hide this class of regression.
2. **Playback startup** — Track manifest request, DRM license request, first decoded frame, and controls-ready time separately.
3. **Memory with video active** — Measure carousels and overlays while a video surface is mounted; image caches and video buffers share the same low memory budget.
4. **Focus navigation FPS** — Measure row-to-row and card-to-card movement, not only inertial scrolling.
5. **Device tier fallback** — Disable heavy shadows, gradients, parallax, and long transitions on low-end devices before reducing content density.
## Automating Performance Measurements
Manual testing doesn't scale across TV platforms:
```jsx
import { PerformanceObserver, performance } from 'react-native-performance';
performance.mark('app_start');
AppRegistry.registerComponent(appName, () => {
performance.mark('app_registered');
return App;
});
```
- Collect timestamps via automated tests on real devices
- Push metrics to Grafana/Datadog for trend tracking
- Fail CI if metrics regress beyond thresholds
## Related Skills
- [perf-animations.md](./perf-animations.md) — Animation performance
- [perf-lists.md](./perf-lists.md) — List virtualization
- [perf-network.md](./perf-network.md) — Network optimization
- [perf-memory.md](./perf-memory.md) — Memory management
- [focus-performance.md](./focus-performance.md) — Focus-related performance
references/release-cicd.md
---
title: CI/CD for TV Apps
impact: MEDIUM
tags: cicd, build-fingerprinting, diff-triggers, app-store, multi-platform, tv
---
# CI/CD for TV Apps
In TV development, a simple "build and test" pipeline explodes in complexity. Every step multiplies by the number of platforms and targets you support.
## Quick Reference
- TV CI/CD = mobile pipeline × 6+ platforms × multiple device SKUs
- Run static, unit, and integration checks before device-heavy E2E
- **Build fingerprinting** — skip native builds when only JS changed
- **Diff-based triggers** — only build platforms affected by changes
- Standard CI caching (node_modules, Gradle, CocoaPods) applies as usual — it just pays off more here because every cache hit is multiplied across N platform targets
## The Multiplication Problem
Mobile: install → validate → build (iOS, Android) → bundle → E2E
TV: same steps × tvOS, Android TV, Fire TV, webOS, Tizen, Vega OS. Native builds and device E2E dominate runtime, so avoid running them when inputs did not change.
## Move Work Out of Device E2E
Shift E2E tests into faster integration tests using RNTL:
- Abstract platform-specific quirks (D-pad keycodes)
- Cover JS-owned state transitions before launching devices
- Keep device E2E for native focus-engine behavior, launch, routing, playback startup, and platform packaging
- Share integration scenarios across platforms, then run platform-specific E2E only for behavior the JS layer cannot prove
## Build Fingerprinting
Most PRs don't modify native code — only JS. Fingerprinting generates a hash of everything that influences the native build:
```bash
npx expo fingerprint:generate --platform ios,android,tvos
# Compare in CI:
if [ "$(cat .last_fingerprint)" = "$(cat .current_fingerprint)" ]; then
echo "No native changes — skipping rebuild."
else
echo "Native changes detected — rebuilding..."
fi
```
**Sources included:** `ios/Podfile`, `android/build.gradle`, `app.json`, `package.json`
## Diff-Based Triggers
Only build what actually changed:
```yaml
# GitHub Actions example
name: TV CI
on:
pull_request:
paths:
- 'packages/common/**'
- 'apps/tvos/**'
- 'apps/androidtv/**'
```
Map directories to platforms → trigger only affected builds.
## Performance in CI
Embed performance markers in your app:
```jsx
import { performance } from 'react-native-performance';
performance.mark('app_start');
AppRegistry.registerComponent(appName, () => {
performance.mark('app_registered');
return App;
});
```
Collect via automated tests on:
- AWS Device Farm for Fire TV / Android TV
- Local device rack for tvOS and Tizen
- Push metrics to Grafana/Datadog
- Fail CI if cold start exceeds KPI targets by even 10%
## App Store Requirements
Different platforms have different review processes:
- **Amazon Fire TV** — Amazon Appstore submission
- **Android TV** — Google Play Store with TV-specific requirements
- **Apple TV** — App Store review (tvOS-specific guidelines)
- **webOS / Tizen** — Platform-specific submission portals
## Related Skills
- [test-strategy.md](./test-strategy.md) — Testing approach and tools
- [test-e2e.md](./test-e2e.md) — E2E testing and device farms
- [setup-architecture.md](./setup-architecture.md) — Multi-platform project structure
references/setup-architecture.md
---
title: Codebase Architecture and Sharing
impact: MEDIUM
tags: architecture, code-sharing, monorepo, platform-extensions, cross-platform, tv
---
# Codebase Architecture and Sharing
The choice of structure depends on your project's scope and whether your app is part of a larger multi-platform product.
## Quick Reference
- Monorepo is the most common structure for TV apps (platforms require bundled applications)
- Reuse business logic, state, hooks, and API layers before reusing screen UI
- Expect TV screen UI, focus behavior, and platform packaging to need dedicated implementations
- Use TV-specific Metro extensions (`.ios.tv.*`, `.android.tv.*`, `.tv.*`) only when the project has enabled them in Metro
## Multi-Platform TV Shape
```
my-tv-app/
├── ios/ # iOS native files
├── tvos/ # tvOS native files
├── android/ # Android, Android TV, Fire TV native
├── vega/ # Vega OS native files
├── web/ # web, webOS, Tizen native files
├── src/ # Application code (shared)
│ ├── index.web.tsx # Web entry point
│ └── index.js # Native entry point
├── rsbuild.config.ts # Web bundler config
├── metro.config.ts # Native bundler config
└── package.json
```
Use this shape as a detection aid, not a required layout. The important review question is whether TV-native folders and packaging files match the stack detected in [SKILL.md](../SKILL.md).
## Platform Detection
```jsx
import { Platform } from 'react-native';
if (Platform.isTV) {
// TV-specific logic
}
```
## Platform-Specific Components
For UI that differs by focus model or TV layout, use file extensions:
```
MyComponent.ios.tv.tsx
MyComponent.android.tv.tsx
MyComponent.tv.tsx
MyComponent.ios.tsx
MyComponent.android.tsx
```
`react-native-tvos` documents this resolution order for projects that opt into TV extensions in Metro: platform-specific TV file, generic TV file, then normal platform file. This Metro configuration is not enabled by default because it can affect bundling performance.
## Platform-Specific Styles
`Platform.select` keys match `Platform.OS`, not the marketing name — there are no `tvOS`/`fireTV` keys. `Platform.OS` is `ios` on Apple TV and `android` on Android TV / Fire TV (`react-native-tvos`), but `kepler` on Vega (see [Vega OS](#vega-os) below). Gate TV-only logic with `Platform.isTV`:
```jsx
import { StyleSheet, Platform } from 'react-native';
const styles = StyleSheet.create({
container: {
...Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.2 }, // tvOS uses shadow*
android: { elevation: 4 }, // Android TV / Fire TV
// Vega (Platform.OS === 'kepler') matches neither — add a `kepler` key if needed
}),
...(Platform.isTV ? { padding: 24 } : {}),
},
});
```
## Android TV Setup
Minimal changes — same APK runs on TV:
- Add `android.software.leanback` support in manifest
- Add `LEANBACK_LAUNCHER` intent filter
## tvOS Setup
Needs a separate `tvos/` folder (copy of `ios/` with modifications) due to CocoaPods setup. Initialize from template and move the generated `ios/` folder.
## Web-Based TVs (webOS, Tizen)
Put web-native code in `web/` folder. Use Rsbuild (or your preferred bundler) for web builds with server host `0.0.0.0` for TV device discovery.
## Vega OS
Standalone setup using React Native 0.72 (React 18). Follow official Vega OS docs. Code sharing may be limited by React 18/19 API differences.
## Related Skills
- [setup-getting-started.md](./setup-getting-started.md) — Project creation and dependencies
- [setup-cross-platform.md](./setup-cross-platform.md) — Handling platform inconsistencies
- [release-cicd.md](./release-cicd.md) — CI/CD for multi-platform TV apps
references/setup-cross-platform.md
---
title: Handling Cross-Platform Inconsistencies
impact: HIGH
tags: cross-platform, platform-detection, platform-select, spatial-navigation, tv
---
# Handling Cross-Platform Inconsistencies
When building for both mobile and TV, small platform differences add up. Centralize platform-specific logic and leverage libraries with built-in platform support.
## Quick Reference
- Use `Platform.isTV` for conditional TV logic
- Use platform-specific file extensions for drastically different UI
- Abstract platform-specific styles with `Platform.select()`
- Many libraries (react-navigation, react-native-gesture-handler) handle platform quirks internally
## Platform Detection
```jsx
import { Platform } from 'react-native';
if (Platform.isTV) {
// TV-specific logic
}
```
Centralize platform checks in utility functions rather than scattering them throughout components.
## Platform-Specific Files
```
MyComponent.tvos.js
MyComponent.ios.js
MyComponent.android.js
```
React Native automatically selects the correct file for the running platform.
## Platform-Specific Styles
`Platform.select` keys match `Platform.OS`, not the marketing name — there are no `tvOS`/`fireTV` keys. The value differs per fork: `ios` on Apple TV and `android` on Android TV / Fire TV (`react-native-tvos`), but `kepler` on Vega (`react-native-kepler`). Branch on `Platform.OS` and gate TV-only logic with `Platform.isTV` (which is `true` on all three):
```jsx
const styles = StyleSheet.create({
container: {
...Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.2 }, // tvOS uses shadow*
android: { elevation: 4 }, // Android TV / Fire TV
// Vega (Platform.OS === 'kepler') matches neither — add a `kepler` key if needed
}),
...(Platform.isTV ? { padding: 24 } : {}),
},
});
```
> Note: `react-native-tvos` can't tell Fire TV apart from Android TV via `Platform` — both report `android`. Use device manufacturer info for that distinction.
## Third-Party Libraries
Check if a library already addresses your cross-platform needs before building custom solutions:
- **react-navigation** — Handles navigation patterns across platforms
- **react-native-gesture-handler** — Platform-aware gesture handling
- **@bamlab/react-tv-space-navigation** — Spatial navigation across TV platforms
- **@noriginmedia/norigin-spatial-navigation** — For web-based TV platforms
## nextFocus* for Cross-Platform
`nextFocusUp`, `nextFocusDown`, etc. on `View` are honored by the **Cartesian focus engines** (Android TV, Fire TV, Vega OS) and by **tvOS** — with a tvOS caveat: the override is ignored when no focusable view exists in that direction. For shared codebases, prefer `TVFocusGuideView` and inferred focus as the default, and use `nextFocus*` only as a targeted override where that caveat is acceptable. See [focus-management.md](./focus-management.md) for the full rule.
## react-native-tvos Compatibility
The `react-native-tvos` fork does not prevent mobile builds. It extends core with TV-specific features while maintaining API compatibility. Mobile app logic stays intact.
## Related Skills
- [setup-getting-started.md](./setup-getting-started.md) — Project setup
- [setup-architecture.md](./setup-architecture.md) — Code sharing strategies
- [focus-management.md](./focus-management.md) — Cross-platform focus handling
references/setup-getting-started.md
---
title: Getting Started with React Native for TV
impact: MEDIUM
tags: setup, react-native-tvos, expo, tvos, android-tv, getting-started
---
# Getting Started with React Native for TV
Use this reference only after stack detection identifies a `react-native-tvos` or Expo TV app. For Amazon Vega/Kepler or web-based TV targets, use the platform toolchain instead; do not require `react-native-tvos`, a tvOS Podfile, or Android TV manifest entries there.
`react-native-tvos` is an independent React Native fork for Apple TV, Android TV, and Fire TV. It tracks React Native core while adding TV focus, remote input, and platform APIs.
## Quick Reference
- Use `react-native-tvos` as a drop-in replacement for `react-native`
- For Expo, use the TV templates or `@react-native-tvos/config-tv` and keep the `react-native-tvos` version aligned with the Expo SDK
- The fork does NOT prevent building regular iOS/Android mobile apps
- TV support adds focus handling, remote input, and TV-optimized components
## Without Expo (React Native CLI)
### New Project
```bash
npx @react-native-community/cli@latest init TVTest \
--template @react-native-tvos/template-tv
```
### Existing Project
Replace `react-native` in `package.json`:
```json
"react-native": "npm:react-native-tvos@latest"
```
#### Android TV Setup
Add to `AndroidManifest.xml`:
```xml
<intent-filter>
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.faketouch" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="true" />
```
> Add these to TV-specific manifest only. Mobile builds still need touchscreen.
#### Apple TV Setup
- Update `project.pbxproj` to include tvOS platform
- In Podfile: `platform :tvos, min_ios_version_supported`
- Current `react-native-tvos` app Podfiles support either an iOS target or a tvOS target; do not keep both targets in the same Podfile
## With Expo
### New Project
```bash
npx create-expo-app MyTVProject -- -e with-tv
# Or with navigation:
npx create-expo-app MyTVProject -- -e with-router-tv
```
### Existing Expo Project
1. Replace react-native:
```json
"react-native": "npm:react-native-tvos@0.85-stable"
```
2. Match the `react-native-tvos` version to the Expo SDK's React Native version. For SDK 56+, Expo upgrades this dependency with SDK upgrades; for SDK 55 and earlier, upgrade it manually and exclude it from `expo install` validation:
```json
"expo": { "install": { "exclude": ["react-native"] } }
```
3. Install TV plugin:
```bash
npx expo install @react-native-tvos/config-tv -- --dev
```
4. Add to `app.json`:
```json
{ "plugins": ["@react-native-tvos/config-tv"] }
```
5. Build. The plugin runs when `EXPO_TV=1` is set, or when its `isTV` plugin option is true:
```bash
export EXPO_TV=1
npx expo prebuild --clean
```
## Environment Setup
Same as React Native mobile, plus:
- **Android:** Download TV system image in SDK Manager, create Android TV emulator
- **Apple TV:** Install tvOS SDK via `xcodebuild --downloadAllPlatforms`
## Key API Differences from Core React Native
| Component / API | TV Changes |
|----------------|------------|
| `Platform` | Added `Platform.isTV` (any TV) and `Platform.isTVOS` (Apple TV only). No `isAndroidTV` flag — detect with `Platform.OS === 'android' && Platform.isTV`. Fire TV needs device info (manufacturer), not a `Platform` flag. |
| `Pressable`, `TouchableHighlight`, `TouchableOpacity` | Native `onFocus` & `onBlur` events + remote mapping |
| `TouchableNativeFeedback`, `TouchableWithoutFeedback` | Press events work, but focus/blur events do not; avoid for TV focusable controls |
| `Pressable` | `.focus:` and `.active:` Tailwind pseudo classes |
| `TVEventHandler` / `useTVEventHandler` | Custom remote event handling |
| `TVFocusGuideView` | Focus management between disconnected areas |
| `View` | `nextFocus*` props for directional focus overrides (Cartesian platforms — Android TV, Fire TV, Vega OS — plus tvOS with a caveat; see [focus-management.md](./focus-management.md)) |
| `ScrollView` | TV-only snap/focus props such as `snapToAlignment="item"`, `scrollSnapAlign`, `scrollSnapOffset`, and `scrollAnimationEnabled` |
| `VirtualizedList` | Extended for focus management, including `additionalRenderRegions` for critical always-rendered ranges |
| `BackHandler` | Extended for Apple & Android TV back button |
| `TVTextScrollView` (Apple TV) | Scrolling via swipe gestures from remote |
| `TVEventControl` (Apple TV) | Enable/disable Siri remote features |
## Community Resources
- **Ignite TV** — Boilerplate from Infinite Red for TV apps
- **Amazon Sample Apps** — Multi-platform TV best practices
- **Hoppix** — Demo showing spatial navigation on TV
- **@bamlab/react-tv-space-navigation** — Spatial navigation across platforms
## Related Skills
- [setup-architecture.md](./setup-architecture.md) — Project structure and code sharing
- [setup-cross-platform.md](./setup-cross-platform.md) — Handling platform differences
references/test-e2e.md
---
title: End-to-End Testing for TV Apps
impact: MEDIUM
tags: e2e, appium, webdriverio, device-farms, tvos, android-tv
---
# End-to-End Testing for TV Apps
For full behavioral testing, Appium is the best option for React Native TV. It supports Android TV and Apple TV via UIAutomator and XCUITest. Web-based platforms test through browser automation.
## Quick Reference
- Use Appium + WebdriverIO for native TV platforms
- Use `driver.pressKeyCode` to simulate D-pad navigation
- Use accessibility labels as selectors (`~home-button`)
- Device farms (AWS, BrowserStack, Sauce Labs) for real hardware testing
## Appium Setup — Android TV
```typescript
// wdio.conf.ts
capabilities: [{
platformName: 'Android',
automationName: 'UiAutomator2',
deviceName: 'Android TV Emulator',
appPackage: 'com.mycompany.tvapp',
appActivity: 'com.mycompany.tvapp.MainActivity',
newCommandTimeout: 300,
}]
```
## Appium Setup — Apple TV
```typescript
capabilities: [{
platformName: 'iOS',
platformVersion: '17.0',
deviceName: 'Apple TV',
automationName: 'XCUITest',
udid: 'auto',
app: '/path/to/your/TVApp.app',
newCommandTimeout: 300,
}]
```
## Example Test
```typescript
describe('TV App Navigation', () => {
it('navigates to Home and selects an item', async () => {
const homeButton = await $('~home-button');
await homeButton.click();
await driver.pressKeyCode(20); // DPAD_DOWN
await driver.pressKeyCode(23); // DPAD_CENTER
const detailsSection = await $('~details-section');
await expect(detailsSection).toBeDisplayed();
});
});
```
Make components accessible for selectors:
```jsx
<Pressable accessibilityLabel="home-button" onPress={goHome}>
<Text>Home</Text>
</Pressable>
```
## Web-Based TV Platforms (Tizen, webOS)
Use WebdriverIO with browser capabilities:
```typescript
capabilities: [{
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--window-size=1920,1080'],
},
}],
services: ['chromedriver'],
```
```typescript
describe('Web TV App (webOS)', () => {
it('navigates with keyboard and selects item', async () => {
await browser.url('http://localhost:1234/index.html');
const homeButton = await $('aria/Home');
await homeButton.click();
await browser.keys(['ArrowDown', 'Enter']);
const details = await $('[data-testid="details"]');
await expect(details).toBeDisplayed();
});
});
```
## Device Farms
Real-device testing is essential — emulators can't replicate remote input, performance, or display quirks.
| Service | Supported | Integration |
|---------|-----------|-------------|
| AWS Device Farm | Android, iOS, custom | Upload APK/IPA, use ARN refs |
| BrowserStack | Android, iOS, web | `bs://` app IDs, wdio service |
| Sauce Labs | Android, iOS, web | `storage:` app refs |
### Running on Device Farms
1. Upload binary (APK/IPA) via API
2. Get app ID (ARN, bs://, storage:)
3. Update capabilities with real device names
4. Run: `npx wdio run wdio.browserstack.conf.ts`
### AWS Device Farm
```bash
aws devicefarm schedule-run \
--project-arn arn:... \
--app-arn arn:... \
--device-pool-arn arn:... \
--name "MyApp TV Run" \
--test type=APPIUM_NODE,testPackageArn=arn:...
```
## Limitations
- **Maestro and Detox** are not helpful for TV environments
- Web-based TVs run custom browser forks — never exact match in automated tests
- For platform-specific quirks, real devices are the only reliable test
## Related Skills
- [test-strategy.md](./test-strategy.md) — Overall testing approach
- [test-javascript.md](./test-javascript.md) — JS-level tests with tvRemote helpers
- [release-cicd.md](./release-cicd.md) — CI/CD pipeline integration
references/test-javascript.md
---
title: JavaScript Tests for TV Apps
impact: MEDIUM
tags: testing, rntl, tvremote, focus, hardware-key-events, tv
---
# JavaScript Tests for TV Apps
TV tests use the same React Native Testing Library but need custom helpers for remote-controlled navigation — you can't emulate D-pad with click events.
## Quick Reference
- Create a local `tvRemote` helper for focus/blur/press events owned by JS
- Focus movement must be explicit; RNTL does not run the native TV focus engine
- Test native focus-engine behavior in E2E, not in JS-only tests
- Add platform-specific event-emitter coverage only when app code subscribes to those events
## Example Test
`tvRemote` is a project-specific helper, not a library export.
```jsx
import { render, screen, fireEvent } from '@testing-library/react-native';
import { tvRemote } from './testUtils/tvRemote';
it('navigates and selects the play button', () => {
const onPressMock = jest.fn();
render(<MyComponent onPress={onPressMock} />);
const infoButton = screen.getByRole('button', { name: 'Info' });
const playButton = screen.getByRole('button', { name: 'Play' });
fireEvent(infoButton, 'focus');
tvRemote.right({ elementToFocus: playButton, elementToBlur: infoButton });
tvRemote.select({ elementToSelect: playButton });
expect(onPressMock).toHaveBeenCalled();
});
```
## Building the tvRemote Helper
Start with the smallest helper that matches what React Native Testing Library can actually test: JS focus/blur handlers and press handlers. Keep native focus-engine assertions in E2E.
```jsx
import { fireEvent } from '@testing-library/react-native';
export const tvRemote = {
move({ elementToBlur, elementToFocus } = {}) {
if (elementToBlur) {
fireEvent(elementToBlur, 'blur');
}
if (elementToFocus) {
fireEvent(elementToFocus, 'focus');
}
},
right(args) {
this.move(args);
},
left(args) {
this.move(args);
},
up(args) {
this.move(args);
},
down(args) {
this.move(args);
},
select({ elementToSelect } = {}) {
if (!elementToSelect) return;
fireEvent(elementToSelect, 'pressIn');
fireEvent.press(elementToSelect);
fireEvent(elementToSelect, 'pressOut');
},
};
```
## Testing Native Remote Event Subscribers
If app code subscribes to `TVEventHandler`, `useTVEventHandler`, `DeviceEventEmitter`, or a Vega/Kepler equivalent, add a second helper that emits the event payload shape used by that app. Keep this helper local because payload names differ by platform and RN fork.
```jsx
import { act } from '@testing-library/react-native';
import { DeviceEventEmitter, Platform } from 'react-native';
export function emitRemoteEvent(eventType) {
const payload = Platform.isTVOS
? { eventType }
: { eventType, eventKeyAction: 1 };
act(() => {
DeviceEventEmitter.emit('onHWKeyEvent', payload);
});
}
```
> `Platform.isTVOS` is specific to `react-native-tvos`. For Vega/Kepler, use that stack's documented platform flags and event payloads instead of copying this branch.
## Why Focus Must Be Explicit
The native focus engine handles actual focus movement. In JavaScript tests, there is no real focus search, so specify `elementToFocus` and `elementToBlur` manually. Use E2E to validate that a physical remote press moves focus to the expected element.
## Performance Testing with Reassure
Reuse integration test scenarios to measure render characteristics:
```jsx
// Same RNTL tests, but Reassure measures render times
// Compare results against a stable baseline
```
## Related Skills
- [test-strategy.md](./test-strategy.md) — Overall testing approach
- [test-e2e.md](./test-e2e.md) — End-to-end testing with Appium
- [focus-management.md](./focus-management.md) — Focus APIs being tested
references/test-strategy.md
---
title: Testing Strategy for React Native TV Apps
impact: MEDIUM
tags: testing, integration-tests, rntl, focus, remote-input, tv
---
# Testing Strategy for React Native TV Apps
TV testing should prove the remote-controlled paths that break differently from mobile: focus order, Back/Menu behavior, player controls, low-memory carousels, and platform packaging.
## Quick Reference
- Use integration tests for JS-owned focus state, player-control state, and remote event handlers
- Use E2E tests for native focus engine behavior, app launch, routing, playback startup, and Back/Menu behavior
- For agent-run accessibility smoke, load the `agent-device` skill and read `agent-device help workflow`, then inspect labels, roles, states, and focused elements from the accessibility tree
- Use real hardware for overscan, remote latency, memory pressure, video decode, DRM, and display/color checks
- Keep emulators/simulators for fast route, focus, and smoke coverage; do not treat them as final device validation
- Reuse the same user flows for performance baselines where possible
## JS Integration Tests
Prefer a saved or generated app state that includes rows, entitlement state, player state, and modal state:
```jsx
const snapshot = require('my-state.json');
const { Wrapper } = loadStateFromSnapshot(snapshot);
render(<Wrapper><VideoPlayer /></Wrapper>);
```
- Mock native player/focus modules when JS tests cannot load them
- Mock timers for auto-hide controls, debounce, and "are you still watching" flows
- Avoid mocking app state in tests that are meant to prove focus restoration or navigation paths
See [test-javascript.md](./test-javascript.md) for the local `tvRemote` helper pattern.
## CI Scope
- PR checks: static checks, unit tests, integration tests, and changed-platform build checks
- Nightly/release checks: E2E on representative TV devices, playback startup, `agent-device` accessibility-tree smoke, memory-sensitive carousel flows
- Device matrix: at least one Apple TV target, one Android TV/Fire TV target, and any required Vega/Tizen/webOS target
## Related Skills
- [test-javascript.md](./test-javascript.md) — JS test setup and tvRemote helpers
- [test-e2e.md](./test-e2e.md) — E2E testing with Appium and device farms
- [perf-overview.md](./perf-overview.md) — Performance KPIs to test
references/video-debugging.md
---
title: Debugging Video Streams
impact: HIGH
tags: video, debugging, ffmpeg, ffprobe, charles, proxyman, profiling
---
# Debugging Video Streams
## Quick Reference
- Inspect the stream with `ffprobe` before changing React player code
- Verify manifest requests, DRM license exchange, ABR switches, and decoder support separately
- Use a proxy for network/license failures; use RN/React tooling for duplicate UI requests or player state desync
- Correlate client-side player errors with server-side CDN/license telemetry
## Playback Failure Layers
1. **Media package** — Use `ffprobe` for codec, bitrate, resolution, audio tracks, subtitles, and container details.
2. **Manifest validity** — Use platform validators where available, such as `mediastreamvalidator` for Apple HLS.
3. **Network path** — Inspect manifest, segment, and license requests with Charles or Proxyman.
4. **DRM/license** — Verify license URL, headers, entitlement token, and hardware security-level failures before changing UI code.
5. **React/player state** — Use Rozenite, React Native DevTools, or app logs for duplicate play requests, hidden controls, stale state, or JS-generated manifest URL changes.
## Network Traffic Analysis
Install the proxy CA certificate on the simulator, emulator, or device before expecting HTTPS manifests or license requests to decrypt.
## Related Skills
- [video-streaming.md](./video-streaming.md) — Streaming architecture
- [video-players.md](./video-players.md) — Player implementations
- [perf-overview.md](./perf-overview.md) — Overall performance strategy
references/video-players.md
---
title: Video Players for React Native TV
impact: HIGH
tags: video, players, react-native-video, exoplayer, avplayer, shaka, drm
---
# Video Players for React Native TV
## Quick Reference
- Choose the player after the target platform and DRM/protocol path are known
- Native TV targets usually end at AVPlayer/ExoPlayer through a wrapper or native module
- Web-based TV targets can use Shaka, hls.js, or dash.js in the browser context
- For seek thumbnails, prefer BIF or another indexed single-file format over many image requests
## Available Players
| Player | Platform | Best For |
|--------|----------|----------|
| AVPlayer | iOS, tvOS | Native Apple playback, FairPlay DRM |
| ExoPlayer | Android TV, Fire TV | Wide format support, Widevine DRM |
| react-native-video | Cross-platform | Wraps AVPlayer + ExoPlayer; easiest setup |
| react-native-theoplayer | Cross-platform | THEOplayer SDK wrapper |
| Shaka Player | JS (all platforms) | DASH + HLS, advanced ABR, multiple DRMs |
| hls.js | Web-based TVs | HLS playback in browsers |
| dash.js | Web-based TVs | MPEG-DASH reference player |
## Player-Control Checks
- Do not mount multiple hidden player instances for preview + main playback unless the target device can decode them concurrently.
- Keep player controls remote-focusable even when video is buffering or DRM is negotiating.
- Separate seekbar focus state from playback progress state so rapid scrubbing does not fight `onProgress`.
- Tear down preview playback before starting protected main content on memory-constrained devices.
## Thumbnail Generation — BIF Format
The Broadcast Image Format bundles all thumbnails in one indexed binary file:
- Single network request (vs. individual image downloads)
- Indexed structure for instant lookup by timestamp
- `thumbIndex = Math.floor(videoTime / interval)`
For large videos (2+ hours), use a native module for BIF parsing. Cache BIF files locally.
## Focus During Scrubbing
- **Debounce thumbnail updates** (100-200ms) during rapid scrubbing
- **Preload adjacent thumbnails** during idle moments
- **Lazy load** distant timeline parts to optimize memory
## Shaka Player for Enterprise
For complex streaming (live sports, multi-DRM, custom ABR), choose the architecture explicitly:
- **Web-based TV targets:** Run Shaka in the TV browser/webview context and render controls in the web UI.
- **Native React Native targets:** Prefer AVPlayer/ExoPlayer through `react-native-video`, THEOplayer, or a custom native module. If Shaka is required for packaging/ABR logic, treat it as an orchestration layer and bridge the native playback surface deliberately; do not assume browser Shaka drops into a native RN view unchanged.
## When to Use What
| Scenario | Recommendation |
|----------|---------------|
| Basic HLS/MP4 playback | react-native-video |
| Simple DRM (single platform) | react-native-video with DRM config |
| Enterprise multi-DRM, live sports | Shaka Player or native players |
| Web-based TVs (Tizen, webOS) | Shaka Player, hls.js, or dash.js |
## Related Skills
- [video-streaming.md](./video-streaming.md) — Streaming architecture and protocols
- [video-debugging.md](./video-debugging.md) — Debugging tools
- [focus-management.md](./focus-management.md) — Focus handling during playback
references/video-streaming.md
---
title: Video Streaming on TV
impact: HIGH
tags: video, streaming, drm, hls, dash, widevine, fairplay, tv-platforms
---
# Video Streaming on TV
Use this reference to choose the TV platform playback path and to classify playback failures by protocol, DRM, decoder, or memory pressure.
## Quick Reference
- Pick protocol/DRM per target platform; do not assume one stream package covers every TV device
- Verify hardware DRM level and decoder capability before changing React player controls
- Tear down unused preview/player instances before starting another stream
- Keep video buffer sizing conservative on 1-2 GB TV devices
## Pick DRM/Protocol by TV Platform
| Platform | Native player | DRM | Protocol |
|----------|---------------|-----|----------|
| Apple TV (tvOS) | AVPlayer | FairPlay | HLS |
| Android TV / Google TV | ExoPlayer | Widevine | DASH (or HLS) |
| Fire TV | ExoPlayer | Widevine (PlayReady on some SKUs) | DASH |
| webOS / Tizen | platform web player | Widevine / PlayReady | DASH (HLS varies) |
A cross-platform app typically ships an HLS+FairPlay path for Apple and a DASH-based path for other platforms, with Widevine or PlayReady chosen per device support; plan the encode/packaging for both.
## TV Hardware Constraints That Bite
- **Security level is enforced in hardware.** Widevine L1 / FairPlay require hardware-backed decryption for HD/4K; low-end SKUs may only offer L3 (SD-capped). Detect and degrade gracefully rather than failing playback.
- **Decoders are limited and shared.** A TV may decode one 4K stream at a time; trailers + main content can contend. Tear down players you aren't using.
- **Memory is shared with the video buffer** — see [perf-memory.md](./perf-memory.md). Oversized buffers on a 1–2 GB device cause OOM, not just jank.
DRM in any case requires a valid license server, app entitlements/permissions, and sometimes native player configuration (security level, hardware decryption).
## Related Skills
- [video-players.md](./video-players.md) — Player implementations and custom controls
- [video-debugging.md](./video-debugging.md) — Debugging tools for video streams
SKILL.md
---
name: react-native-tv-best-practices
description: 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.
license: MIT
---
# React Native TV Best Practices
## Overview
TV-specific review guidance for React Native-backed apps on Apple TV, Android TV, Fire TV, Amazon Vega/Kepler, and web-based TV targets such as Tizen or webOS.
Use this skill only for TV deltas: remote input, focus engines, 10-foot layout, platform packaging, playback/DRM, low-memory TV hardware, and TV accessibility. For ordinary React Native performance or architecture issues, use [react-native-best-practices](../react-native-best-practices/SKILL.md).
## Skill Format
Reference files are grouped by topic prefix:
- `focus-*`: focus engines, focus guides, focus event performance
- `nav-*`: D-pad navigation, Back/Menu behavior, keyboard/search input
- `design-*`: 10-foot typography, layout, color, focus visibility
- `perf-*`: startup, memory, lists, animation, and network constraints on TV hardware
- `video-*`: playback architecture, DRM/protocol selection, debugging
- `a11y-*`: TV accessibility implementation and audit checks
- `setup-*`: stack detection, setup, architecture, cross-platform behavior
- `test-*` and `release-*`: test coverage, E2E, and CI/release workflows
## When to Apply
Apply this skill when the app targets a TV platform and the work involves:
- Focus movement, visible focus, focus restoration, or remote/D-pad input
- TV layout readability, overscan/safe areas, or 10-foot UI density
- TV player controls, manifests, DRM, decoder support, or playback errors
- Performance on low-memory TV hardware, especially with video or large carousels
- TV accessibility with screen readers, captions, focus order, or remote-only interaction
- Platform setup for `react-native-tvos`, Expo TV, Amazon Vega/Kepler, Tizen, or webOS
## Before You Start — Identify the TV Stack
This skill covers several TV stacks. **Detect which one the app targets before flagging setup issues** — demanding `react-native-tvos`, a tvOS Podfile, or an Android TV manifest on a Vega/Kepler or web-based TV app produces false positives.
| Stack | How to detect | Setup expectations |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **react-native-tvos** (Apple TV, Android TV, Fire TV) | `"react-native": "npm:react-native-tvos@…"` in package.json | tvOS Podfile (`platform :tvos`); Android TV `leanback`/`LEANBACK_LAUNCHER` manifest entries; TV emulator/simulator |
| **Expo + react-native-tvos** | above **plus** `@react-native-tvos/config-tv` in app.json | `EXPO_TV=1` prebuild; `react-native-tvos` version must match the Expo SDK; not all Expo features/libraries are available on TV |
| **Amazon Vega / Kepler** | Vega/Kepler SDK & tooling (`@amazon-devices/*` deps, Kepler manifest); **no** `react-native-tvos` | Amazon's Vega/Kepler toolchain — `react-native-tvos`, tvOS Podfile, and Android TV manifest do **not** apply |
| **Web-based TV** (Tizen, webOS) | web bundler (Rsbuild/webpack) + platform packaging; spatial-nav library | Platform SDK packaging; `@noriginmedia/norigin-spatial-navigation` for focus |
The focus, 10-foot design, performance, accessibility, and player guidance applies across all of these — only the **setup/build** expectations are stack-specific.
## Review Rules
- Resolve the target stack before setup advice.
- Prefer natural focus order and focus guides before imperative focus calls or broad `nextFocus*` maps.
- Treat focus loss, invisible focus, and broken Back/Menu behavior as navigation bugs.
- Check readability, safe areas, and focus states at TV distance before tuning visual details.
- Profile on the weakest supported TV device before reporting performance fixes as complete.
- Separate playback failures by layer: manifest request, DRM license exchange, decoder capability, player state, and React UI controls.
## Priority-Ordered Guidelines
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Focus and D-pad navigation | CRITICAL | `focus-*`, `nav-*` |
| 2 | List, animation, and input performance | CRITICAL | `perf-*` |
| 3 | Playback and DRM failures | HIGH | `video-*` |
| 4 | 10-foot readability and layout | HIGH | `design-*` |
| 5 | TV accessibility | HIGH | `a11y-*` |
| 6 | Stack setup, testing, and release | MEDIUM | `setup-*`, `test-*`, `release-*` |
## Quick Reference
1. Detect the TV stack from package files, manifests, native folders, and platform tooling.
2. Reproduce navigation with the remote or D-pad path, not mouse/touch assumptions.
3. Confirm the focused element is always visible, reachable, and restored after modals/routes.
4. Check playback failures from the network/DRM layer upward before changing React controls.
5. Measure list, animation, memory, and startup work on the weakest supported TV target.
## References
### Focus and Navigation
| File | Impact | Description |
|------|--------|-------------|
| [focus-management.md](references/focus-management.md) | CRITICAL | Focus engines, focus guides, `nextFocus*`, and focus restoration |
| [focus-performance.md](references/focus-performance.md) | CRITICAL | Avoiding frame drops from focus event handling |
| [nav-directional.md](references/nav-directional.md) | CRITICAL | Directional navigation rules across TV platforms |
| [nav-patterns.md](references/nav-patterns.md) | CRITICAL | Global/local navigation, modals, tabs, and Back behavior |
| [nav-keyboard.md](references/nav-keyboard.md) | MEDIUM | Search and text input with remotes |
### Design
| File | Impact | Description |
|------|--------|-------------|
| [design-10foot.md](references/design-10foot.md) | HIGH | 10-foot review heuristics |
| [design-typography.md](references/design-typography.md) | HIGH | TV type sizing and readability |
| [design-layout.md](references/design-layout.md) | HIGH | Safe areas, spacing, carousels, and focus room |
| [design-color.md](references/design-color.md) | MEDIUM | Contrast and TV display color constraints |
### Performance
| File | Impact | Description |
|------|--------|-------------|
| [perf-overview.md](references/perf-overview.md) | HIGH | TV performance targets and profiling order |
| [perf-lists.md](references/perf-lists.md) | CRITICAL | Virtualized rows and poster-heavy lists |
| [perf-animations.md](references/perf-animations.md) | CRITICAL | Focus and transition animation performance |
| [perf-memory.md](references/perf-memory.md) | HIGH | Low-memory TV crashes and image/video pressure |
| [perf-network.md](references/perf-network.md) | HIGH | Remote input, request stalls, and network resilience |
### Video, Accessibility, Setup, Testing
| File | Impact | Description |
|------|--------|-------------|
| [video-streaming.md](references/video-streaming.md) | HIGH | TV platform protocol/DRM selection |
| [video-players.md](references/video-players.md) | HIGH | Player choices and custom controls |
| [video-debugging.md](references/video-debugging.md) | HIGH | Manifest, DRM, codec, and playback debugging |
| [a11y-overview.md](references/a11y-overview.md) | MEDIUM | TV-specific accessibility differences |
| [a11y-implementation.md](references/a11y-implementation.md) | HIGH | Accessible labels, roles, live regions, and focus |
| [a11y-checklist.md](references/a11y-checklist.md) | MEDIUM | Launch accessibility audit checklist |
| [setup-getting-started.md](references/setup-getting-started.md) | MEDIUM | `react-native-tvos` and Expo TV setup |
| [setup-cross-platform.md](references/setup-cross-platform.md) | MEDIUM | Platform detection and cross-platform caveats |
| [setup-architecture.md](references/setup-architecture.md) | MEDIUM | Code sharing and project structure |
| [test-strategy.md](references/test-strategy.md) | MEDIUM | TV testing scope and coverage split |
| [test-javascript.md](references/test-javascript.md) | MEDIUM | JS-level remote/focus test helpers |
| [test-e2e.md](references/test-e2e.md) | MEDIUM | Appium and TV E2E coverage |
| [release-cicd.md](references/release-cicd.md) | MEDIUM | CI, build fingerprinting, and release checks |
## Problem → Skill Mapping
| Symptom | Start Here |
| ------------------------------------ | ---------------------------------------------------------------------------- |
| "Focus jumps to wrong element" | [focus-management.md](references/focus-management.md) → Debugging section |
| "App freezes when scrolling lists" | [perf-lists.md](references/perf-lists.md) → Virtualization |
| "Animations stutter on Fire TV" | [perf-animations.md](references/perf-animations.md) → Native driver |
| "Text too small on TV" | [design-typography.md](references/design-typography.md) → Minimum sizes |
| "Video won't play / DRM errors" | [video-streaming.md](references/video-streaming.md) → DRM section |
| "Screen reader skips elements" | [a11y-implementation.md](references/a11y-implementation.md) → Roles & labels |
| "Back button doesn't work right" | [nav-patterns.md](references/nav-patterns.md) → Back navigation |
| "Keyboard covers content" | [nav-keyboard.md](references/nav-keyboard.md) → Built-in vs custom |
| "App takes forever to start" | [perf-overview.md](references/perf-overview.md) → Startup time |
| "Images causing memory crashes" | [perf-memory.md](references/perf-memory.md) → Image optimization |
| "CI pipeline takes hours" | [release-cicd.md](references/release-cicd.md) → Fingerprinting |
| "How to share code across platforms" | [setup-architecture.md](references/setup-architecture.md) → Code sharing |
## Security (TV-Specific)
General dependency/input hygiene applies as in any RN app; the TV-specific deltas worth calling out:
- Never embed FairPlay/Widevine/PlayReady keys in client code — treat the license server as the trust boundary and keep DRM tokens server-issued.