A 10-foot, TV-style games browse UI driven entirely by a D-pad, gamepad, or arrow keys — no mouse, no touch. Built as a focused demonstration of the spatial-navigation and rendering engineering that a smooth living-room UI requires on low-power hardware.
▶ Live demo: https://tv-game-browser.vercel.app
A TV remote emits directions, not Tab. The tile that should receive focus
when you press "right" is the one physically to the right on screen — which,
in a virtualized carousel, is usually not the next element in DOM order. So focus
can't be a tabindex sequence; it has to be resolved from on-screen geometry.
On top of that, it has to stay at 60fps on a cheap TV SoC, where a single
synchronous image decode or a scrollLeft reflow inside a frame is enough to
drop frames visibly.
This project solves both:
- A geometry-based spatial navigation engine — every focusable element registers its rectangle; a pure scoring function picks the nearest candidate in the pressed direction, with focus memory per row and a modal focus trap.
- A transform-only, virtualized render path plus an off-main-thread image
pipeline (
img.decode(), 4-way concurrency cap, spatially-bounded LRU) so navigation never triggers layout or synchronous decode.
Full write-up in ARCHITECTURE.md.
Under 6× CPU throttling (CDP, emulating a low-power TV SoC), driving 190 key presses across the whole grid:
| Metric | Threshold | Measured |
|---|---|---|
| Dropped frames | ≤ 5% | 0% |
| Longest long task | < 100ms | 0ms (0 observed) |
| JS heap | ≤ 100MB | 9.5MB |
Sustained ~60fps (990 frames / 16.5s). These are real, reproducible numbers — see
BENCHMARK.md for methodology and caveats. Nothing here is
hand-waved; npm run benchmark fails the build if any threshold regresses.
- React 19 + TypeScript (strict,
verbatimModuleSyntax) - Vite 8 build / dev
- Vitest for unit + integration tests
- Playwright + CDP for the performance benchmark
- Deployed on Vercel
- Gamepad API + keyboard for input; no UI framework, no virtualization library — the engine is ~500 lines of first-party code
npm install
npm run dev # http://localhost:5173Navigate with arrow keys (or a connected gamepad's D-pad). Enter/A opens a tile's detail modal; Esc/B closes it.
Tiles are real games with real box art. src/data/games.ts is generated from
Steam's public store API (titles, release years, genres, ratings, descriptions)
with cover art from Steam's library capsules — committed as static data, so the
app ships with real games and needs no API key at runtime. To refresh or recurate:
node scripts/fetch-games.mjs # edit the curated app-ID list at the top firstnpm test # 46 unit + integration tests (vitest)
npm run build # type-check (tsc -b) + production build
npm run benchmark # Playwright perf run under 6x CPU throttleThe integration tests mock getBoundingClientRect to synthesize grid geometry,
then drive real keydown events through the live engine — asserting within-row
and cross-row movement, focus memory, aria-live announcements, and the modal
focus trap.
Roving tabindex, an aria-live announcer for every focus change, a
role="dialog" modal with trapped/restored focus, 48px overscan-safe margins,
24px minimum type, a high-contrast focus ring, and no hover-dependent UI.

