home / skills / dbobkov245-source / pwa-torserve / tv-navigator
This skill helps you create TV-first interfaces with keyboard navigation using useTVNavigation to ensure focus, scrolling, and accessibility.
npx playbooks add skill dbobkov245-source/pwa-torserve --skill tv-navigatorReview the files below or copy the command above to add this skill to your agents.
---
name: tv-navigator
description: Specialist in Android TV D-Pad navigation and React focus management.
---
# TV Navigator Skill
This skill provides expertise in creating "TV-First" interfaces using the `useTVNavigation` hook.
Your goal is to ensure every component is accessible via D-Pad (Arrow Keys) and handles focus states correctly.
## 🧠 Core Concepts
### 1. `useTVNavigation` Hook
Located in: `client/src/hooks/useTVNavigation.js`
**Signature:**
```javascript
const {
focusedIndex, // Current active index (0..N)
setFocusedIndex, // Manually set focus
containerProps, // { onKeyDown, tabIndex } - spreads to parent container
isFocused // Helper: (index) => boolean
} = useTVNavigation({
itemCount: number, // Total items
columns: number, // 1 for List, >1 for Grid
itemRefs: React.RefObject, // { current: { [index]: HTMLElement } }
onSelect: (index) => void, // Enter/OK press
onBack: () => void, // Escape/Back press
loop: boolean, // Default: false
trapFocus: boolean, // true = Isolated (Modals), false = Global (HomeRow)
isActive: boolean // External control. If false, ignores all input.
})
```
### 2. Focus Visualization
- NEVER use `:hover` for TV interfaces.
- ALWAYS use the `.focused` state logic or conditional rendering based on `focusedIndex`.
- For `focused` items, apply: `border`, `transform: scale(1.05)`, or `box-shadow`.
### 3. Scroll Management
The hook automatically handles scrolling using `scrollIntoView({ behavior: 'smooth', block: 'center' })`.
You must attach refs to items:
```javascript
<div ref={el => itemRefs.current[index] = el} ... >
```
### 4. Integration with `activeArea`
The hook must respect the `isActive` flag.
* If `isActive === false`: The hook ignores ALL key presses.
* This allows other UI areas (like the Sidebar) to take over control without unmounting the grid.
## 🛠 Common Patterns
### Vertical List (Menu)
```javascript
const { containerProps, isFocused } = useTVNavigation({
itemCount: items.length,
columns: 1
});
```
### Grid (Posters)
```javascript
const { containerProps } = useTVNavigation({
itemCount: items.length,
columns: 4 // or dynamic based on width
});
```
## ⚠️ Anti-Patterns to Avoid
1. **Hidden Overflow:** Avoid `overflow: hidden` on containers that need to scroll, unless you are implementing virtualized scrolling.
2. **Missing TabIndex:** The container MUST have `tabIndex={0}` (provided by `containerProps`) to capture keyboard events.
3. **Mouse Dependency:** Do not rely on `onClick`. Always map `onSelect` (Enter key) to the same handler.
This skill specializes in Android TV D-Pad navigation and React focus management for TV-first interfaces. It centers on the useTVNavigation hook to make every component reachable via Arrow/Enter/Back keys and to manage visual focus state and scrolling. It helps teams convert mouse-driven UIs into robust, accessible TV experiences.
The core is the useTVNavigation hook which tracks focusedIndex, exposes containerProps (onKeyDown, tabIndex), and provides helpers for checking focus and programmatic focus changes. It wires keyboard events (Arrow keys, Enter, Back) to navigation, calls onSelect/onBack callbacks, and auto-scrolls focused items into view using scrollIntoView. The hook respects isActive and trapFocus flags so multiple UI areas can coexist and modal-like components can isolate input.
How do I ensure an item is visually focused?
Use the isFocused helper or compare index to focusedIndex and render a focused class that applies border, transform: scale(1.05), or box-shadow. Do not use :hover.
What if multiple areas listen for keys?
Use the isActive prop to enable only the currently active area. When false, the hook ignores input so another area can handle navigation.
How is scrolling handled?
The hook calls scrollIntoView({ behavior: 'smooth', block: 'center' }) on the focused item's ref. Ensure you attach refs to every item so scrolling can occur.