tv-spatial-navigation
v1.2.1
Published
Zoneless, signal-based spatial (D-pad) navigation for (Angular) smart-TV apps
Maintainers
Readme
tv-spatial-navigation
Spatial (D-pad) navigation for Angular smart-TV apps — zoneless, signal-based, no manual focus wiring.
The missing piece between your Angular app and the TV remote. On a smart TV there is no touch and no mouse — users move a highlight around the screen with the arrow keys of a D-pad. This library owns that highlight: you register your rows, and it decides where focus goes when the user presses ← ↑ → ↓, based on where things actually sit on screen.
Features
- Zoneless & signal-based. Focus state is an Angular
signal, so it drives change detection directly and works in both zoneless and zone-based consumer apps — noZone.jsrequired. - Spatial arrow-key navigation. ← / → move within a row; ↑ / ↓ move between rows,
ordered by their real on-screen position (
getBoundingClientRect), not registration order. - Sidebar / content zones. A persistent menu is its own zone: ← at the first card jumps to the sidebar, → returns to the exact content spot you left.
- Focus memory. Every row remembers the last item you were on and restores it when you come back.
- Enter / OK activation. Pressing OK on the focused element fires its
click()— remote and mouse share one code path. - Skips hidden rows. Rows hidden with
display:noneorvisibility:hiddenare left out of ↑ / ↓ navigation, so focus never lands somewhere invisible. - Device gating. Auto-detects TV devices (
isTV()) and stays inert everywhere else, so the library never hijacks arrow keys on a desktop or mobile build. Override withsetEnabled(true). - Boundary signals. A
boundarysignal fires when focus hits an edge (top / bottom / left / right) — hook it to bounce, reveal a header, or play a sound. - Auto-scroll into view. The focused element is scrolled into view along only the
axis that needs it (no sideways jump on horizontal moves). Toggle with
setScrollEnabled(). - Tiny. Zero runtime dependencies beyond Angular and
tslib.
Extracted from a production Angular smart-TV streaming app (Android TV / Tizen / webOS).
Install
npm install tv-spatial-navigationRequires Angular 21+. Works in zoneless apps (the default for new Angular projects).
Heads-up — device gating. By default the library only intercepts keys on detected TV devices (
isTV()). On a desktop or mobile build (including local dev) it stays inert on purpose. To try it anywhere, opt in once:constructor(nav: TvNavigationService) { nav.setEnabled(true); }
| focusKey: Signal<string \| null> | Read-only signal of the focused "<regionId>:<index>", or null before anything is focused. |
| boundary: Signal<{ edge: Edge; seq: number } \| null> | Read-only signal that fires when a move is blocked at an edge. seq increments each hit so repeated presses still notify. |
| isTV(): boolean | Heuristic user-agent check for TV devices. Seeds the default enabled state. |
| setEnabled(value: boolean) | Force key interception on or off, overriding isTV() detection. |
| isEnabled(): boolean | Whether the library is currently intercepting keys. |
| setScrollEnabled(value: boolean) | Turn the auto-scroll-into-view behaviour on or off. |
| scrollBehavior: ScrollBehavior | 'smooth' (default) or 'auto' for the scroll-into-view. |
interface Region {
id: string;
items: HTMLElement[];
type?: 'row' | 'sidebar'; // defaults to a content row
}
type Edge = 'top' | 'bottom' | 'left' | 'right';React to an edge with an effect:
effect(() => {
const hit = nav.boundary();
if (hit?.edge === 'bottom') { /* bounce, load more, … */ }
});Roadmap
- Item-level visibility (skip individual hidden cards within a row)
unregisterRegion()for rows removed with@if/*ngIf- Framework-agnostic core (use the engine outside Angular)
License
MIT © Nitesh Kumar Sahu
