Google launched Android TV in 2014, and big brands like Sony, Philips, and Sharp joined in soon after. These smart TVs have become popular over the years, mainly for their apps. But unlike phones or computers, TVs mostly use remote controls, not touchscreens. While this makes navigating apps a bit tricky, that’s where React Native shines.
React Native is great for creating apps that not only work well on various devices but also make navigating with a TV remote easy and user-friendly. React Native stands out in helping developers design TV apps that are simple to use, no matter the device.
In this article, we’ll talk about how to build a cross-platform React Native TV app with smooth, natural navigation that solves the problem of spatial navigation. We’ll cover each step required to make this work and share tips to make your app user-friendly on any TV.
Here are some suggested prerequisites before diving into this tutorial:
We’re also going to use the react-tv-space-navigation library in our project. You can find the source code for our demo project on GitHub. Let’s start by introducing this library.
With more people wanting TV apps, learning React Native becomes increasingly relevant. Developing a cross-platform TV app with React Native is key for developers entering the expanding smart TV app market.
An essential aspect of this is mastering the use of the react-tv-space-navigation library. Trying to implement this feature from scratch would require a lot of coding to produce similar results. Using a prebuilt library ensures all desired features and customizations are readily available and easy to implement.
The react-tv-space-navigation docs describe the library’s purpose and functionality very well:
React-tv-space-navigation is a library designed to solve spatial navigation in TV apps, a challenge when users rely on remote controls rather than touchscreens. While React Native TV offers a basic solution, React-tv-space-navigation excels by being cross-platform, perfect for apps on AndroidTV, tvOS, and web TV. Built on LRUD (Left, Right, Up, Down), a UI-agnostic framework, it wraps the core spatial navigation logic into a React-friendly package. This makes it easier to develop intuitive and user-friendly TV apps that work seamlessly across different platforms.
As I mentioned in the introduction, most TVs use remote controls, not touchscreens. The react-tv-space-navigation library helps developers overcome this challenge, enabling them to create apps that are not only compatible with various TV models but also easy to navigate with a remote.
This integration is a significant aid for developers striving to make user-friendly and accessible TV apps.
Although react-tv-space-navigation is a great library aiming to be a cross-platform solution, achieving 100 percent compatibility across all platforms can be challenging. Most existing solutions, including those integrated within React Native TV, do not offer complete cross-platform support.
The library’s documentation itself acknowledges this limitation. However, the react-tv-space-navigation team has put in some admirable effort to achieve this goal.
Despite this, react-tv-space-navigation may have a smaller community of users and contributors compared to more widely used libraries such as ReNative or Norigin Spatial Navigation. This can impact the availability of resources, tutorials, and community support for troubleshooting or implementing advanced features.
Well, that’s why I wrote this tutorial: to serve as a unique guide for you.
Spatial scrolling refers to the method of navigating through a digital interface using direction-based commands, typically in a 2D space. This is often seen in environments where a traditional pointer or touchscreen is not available, like with TV remotes or game controllers.
In spatial scrolling, users move through items on the screen using directional inputs (up, down, left, right) rather than by directly selecting them.
Spatial navigation is really handy for gadgets that don’t have touchscreens, like TVs and game consoles, because it lets you move around without needing to scroll. It’s also great for people who might find it tough to use touch or mouse controls, such as those with mobility issues.
This type of navigation offers many benefits, including:
This navigation pattern has found its place in several key applications, enhancing user experience and providing seamless interaction across different platforms and devices. Some of these applications include:
Now, let’s begin working on our demo React Native TV app.
To set up a React Native app, navigate to a directory of choice on your local machine, open up a command window in that directory, and enter the following bash scripts in it:
npx create-expo-app react_spatialtv
The command above creates a React Native app called react_spatialtv
. Our Expo installation provides features to run your Android application on your Android or Apple mobile devices by scanning a QR code and using the Expo mobile app. You can also execute the application on the web.
After setting up a React Native application, we will need to install the react-tv-space-navigation
dependency using the following command in the CLI:
npm install react-tv-space-navigation
To make styling our application easier, we will use NativeWind, an optimized TailwindCSS library extension for React Native. We can install this tool with the following command:
npm install nativewind npm i --dev [email protected]
Next, we will set up a Tailwind config file:
npx tailwindcss init
Open up the project directory in your preferred code editor and make the following changes to Tailwind.config.js
:
// tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./App.{js,jsx,ts,tsx}", "./<custom directory>/**/*.{js,jsx,ts,tsx}"], theme: { extend: {}, }, plugins: [], }
The code above adds Tailwind support to the various file extensions in the project folder. In the root directory, open the babel.config.js
file and add the NativeWind plugin to it:
// babel.config.js module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"], plugins: ["nativewind/babel"], }; };
In this section, we will build the UI for the spatial TV application. This interface is similar to the movie recommendations interface found on Netflix and Amazon Prime, allowing vertical scrolling as well as a horizontal scrolling through lists.
To begin, open up the project folder in your code editor of choice. For this tutorial, I will execute my React Native app on the web. This is so I can provide larger screenshots for a better visual reference to the workings of the application.
To enable NativeWind Tailwind styles on the web interface, add the following code to the App.js
file:
import { NativeWindStyleSheet } from "nativewind"; NativeWindStyleSheet.setOutput({ default: "native", });
On the web interface, we would not be able to access touch controls for scrolling behavior as would be possible on mobile gadgets. As a workaround, we can set up the keyboard arrow buttons to recreate the scroll function:
// necessary application imports import { StyleSheet, Text, View, Image } from "react-native"; import { Directions, SpatialNavigation, SpatialNavigationVirtualizedGrid, SpatialNavigationRoot, SpatialNavigationNode, SpatialNavigationScrollView, } from "react-tv-space-navigation"; import { NativeWindStyleSheet } from "nativewind"; import { useCallback, useState, useEffect } from "react"; export default function App() { // set up a callback to listen for key press events const subscribeToRemoteControl = (callback) => { const handleKeyPress = configureRemoteControl(); eventEmitter.on("keyDown", callback); return handleKeyPress; }; // handle unsubscription from key remote events const unsubscribeFromRemoteControl = (handleKeyPress) => { KeyEvent.removeEventListener(handleKeyPress); }; SpatialNavigation.configureRemoteControl({ remoteControlSubscriber: subscribeToRemoteControl, remoteControlUnsubscriber: unsubscribeFromRemoteControl, }); SpatialNavigation.configureRemoteControl({ remoteControlSubscriber: (callback) => { const mapping = { ArrowRight: Directions.RIGHT, ArrowLeft: Directions.LEFT, ArrowUp: Directions.UP, ArrowDown: Directions.DOWN, Enter: Directions.ENTER, }; const eventId = window.addEventListener("keydown", (keyEvent) => { callback(mapping[keyEvent.code]); }); return eventId; }, remoteControlUnsubscriber: (eventId) => { window.removeEventListener("keydown", eventId); }, }); // return block }
In the code block above:
subscribeToRemoteControl
callback function that uses an Emitter
to listen for a key-down effectunSubscribeFromRemoteControl
takes a handleKeyPress
param, which is the callback function used to handle key presses, and removes this callback from the list of event listeners for key eventsconfigureRemoteControl
defines a subscriber function that handles the application of subscribeToRemoteControl
and unSubscribeFromRemoteControl
to handle remote control events and stop listening to those events during a key pressNext, we mapped out a list of key events and the corresponding keyboard keys that trigger each event. When one of the defined keys is pressed, it fires the key event listening function and passes the provided callback with the mapped direction.
For spatial space navigation, we would require some movie data to iterate over and display on the user interface. For this, we will create an array as shown below:
const data = [ { id: "1", title: "Avengers: Endgame", year: "2019", description: "After the devastating events of Avengers: Infinity War (2018), the universe is in ruins.", imageUrl: "https://upload.wikimedia.org/wikipedia/en/0/0d/Avengers_Endgame_poster.jpg", }, { id: "2", title: "Avengers: Infinity War", year: "2018", description: "The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.", imageUrl: "https://upload.wikimedia.org/wikipedia/en/4/4d/Avengers_Infinity_War_poster.jpg", }, { id: "3", title: "Avengers: Age of Ultron", year: "2015", description: "When Tony Stark and Bruce Banner try to jump-start a dormant peacekeeping program called Ultron, things go horribly wrong and it's up to Earth's mightiest heroes to stop the villainous Ultron from enacting his terrible plan.", imageUrl: "https://upload.wikimedia.org/wikipedia/en/0/0d/Avengers_Endgame_poster.jpg", }, { id: "4", title: "Avengers: Infinity War", year: "2018", description: "The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.", imageUrl: "https://upload.wikimedia.org/wikipedia/en/4/4d/Avengers_Infinity_War_poster.jpg", }, // add more data entries ]
In the code block above, the movie object we defined contains an id
, a title
, a description
, and an imageUrl
. We would use these attributes to render unique movie cards for our spatial navigation window.
In this section, we will display our movie content and integrate spatial scrolling for the movie data. In the App.js
file, we will use the react-native-tv-space-navigation
dependency to create a component that will display all movie data in the array using a grid format.
Create a new Movies
component and add the following code:
export default function App() { //... previous functions // state to manage side menu toggle const [isMenuOpen, toggleMenu] = useState(false); // detect movement to side menu const onDirectionHandledWithoutMovement = useCallback( (movement) => { if (movement === "right") { toggleMenu(false); } }, [toggleMenu] ); //detect movement to virtualized grids const onDirectionHandledWithoutMovementGrid = useCallback( (movement) => { if (movement === "left") { toggleMenu(true); } }, [toggleMenu] ); const Movies = () => ( <View className=" ml-20"> <Text className=" font-semibold text-xl text-slate-200"> Welcome to my marvel movies App! </Text> <SpatialNavigationRoot onDirectionHandledWithoutMovement={ onDirectionHandledWithoutMovementGrid } > <View className=" h-[700px] bg-[#333] p-10 rounded-lg overflow-hidden"> <SpatialNavigationScrollView horizontal={true} style={{ padding: 20 }} offsetFromStart={10} > <SpatialNavigationVirtualizedGrid data={data} renderItem={renderItem} numberOfColumns={15} itemHeight={200} numberOfRenderedRows={7} numberOfRowsVisibleOnScreen={3} onEndReachedThresholdRowsNumber={2} rowContainerStyle={{ gap: 30 }} scrollBehavior="stick-to-end" /> </SpatialNavigationScrollView> </View> </SpatialNavigationRoot> </View> ); // return block }
In the code block above, we are using the following components imported from the React-tv-space-navigation
library:
SpatialNavigationRoot
SpatialNavigationVirtualizedGrid
SpatialNavigationScrollView
Here, SpatialNavigationRoot
is the root component that serves as the container for any spatial navigation node. The property onDirectionHandledWithoutMovement
assigned to the component will be used to detect changes between this container and a side menu we will add in the latter part of this article.
SpatialNavigationScrollView
enables scrolling of spatial node components in a defined direction. Finally, we used the SpatialNavigationVirtualizedGrid
to display the data
, using a component renderItem
, and also defined the properties of the grid.
To create the renderItem
component, make the following additions to the App.js
file:
export default function App() { //... const renderItem = ({ item }) => { return <FocusableNode item={item} />; }; const FocusableNode = ({ item }) => ( <SpatialNavigationNode isFocusable={true}> {({ isFocused }) => ( <View className={`p-3 relative border-2 rounded-md w-[180px] h-[180px] flex flex-col justify-center items-center gap-4 ${ isFocused ? " border-white" : "" }`} > <View> <Image source={{ uri: item.imageUrl }} className="w-[150px] h-[100px] rounded-md" /> <Text className={`text-white ${isFocused ? "font-bold" : ""} `}> {item.title} </Text> <Text className={`text-white ${ isFocused ? "scale-100 ease-in-out transition-all" : "" }`} > {item.year} </Text> </View> </View> )} </SpatialNavigationNode> ); return ( <View style={styles.container}> <Movies /> </View> ); }
Using the SpatialNavigationVirtualizedGrid
we created individual components for every object in the data
array. Each of these components is rendered using FocusableNode
and display texts and an image.
We also have a property called isFocused
that we use to determine if the spatial node is in focus. When the value of isFocused
is true
, we change the style.
Finally, we are rendering the Movies
component within App
. Execute your React Native app using the npm run start
command. The results would be similar to the GIF below:
To create a side menu on our page, we will create and add a new component that will take an absolute position to the left of the screen and will contain spatial navigation components:
export default function App() { // ... const SideMenu = () => ( <SpatialNavigationRoot onDirectionHandledWithoutMovement={onDirectionHandledWithoutMovement} > <SideMenuElement onSelect={() => toggleMenu(false)} /> </SpatialNavigationRoot> ); const SideMenuElement = ({ onSelect }) => ( <View className={`absolute z-10 left-0 h-full flex justify-center bg-slate-900 ${ isMenuOpen ? "w-24" : "w-6" }`} > <SpatialNavigationNode isFocusable onSelect={onSelect}> {({ isFocused }) => ( <View className={` px-2 text-white py-4 ${isFocused ? "bg-white" : ""}`} > <Text>{isMenuOpen ? "Animations" : "A"}</Text> </View> )} </SpatialNavigationNode> <SpatialNavigationNode isFocusable onSelect={onSelect}> {({ isFocused }) => ( <View className={` px-2 text-white py-4 ${isFocused ? "bg-white" : ""}`} > <Text>{isMenuOpen ? "Movies" : "M"}</Text> </View> )} </SpatialNavigationNode> <SpatialNavigationNode isFocusable onSelect={onSelect}> {({ isFocused }) => ( <View className={` px-2 text-white py-4 ${isFocused ? "bg-white" : ""}`} > <Text>{isMenuOpen ? "BlockBusters" : "B"}</Text> </View> )} </SpatialNavigationNode> </View> ); return ( <View style={styles.container}> <SideMenu /> <Movies /> </View> ); }
In the code block above, we created a SideMenu
component. We are also using the isFocused
property to change the sidebar appearance, and onDirectionHandledWithoutMovement
to detect directional movement to the side menu. In our application, we now have the following results:
Besides react-tv-space-navigation, there are other libraries worth considering or mentioning for creating TV apps that require spatial navigation. A couple of examples include Norigin Spatial Navigation and ReNative. Here’s a quick comparison:
Compared factors | react-tv-space-navigation | ReNative | Norigin Spatial Navigation |
---|---|---|---|
Target platform compatibility | Suitable for TV applications, supporting AndroidTV, tvOS, and web platforms | Suitable for mobile, web, TVs, desktops, consoles, wearables | Suitable for key navigation (directional navigation) on web-browser apps and other browser-based smart TVs and connected TVs |
Development focus | Concentrates on spatial navigation for TV applications with React Native, ensuring that navigation within TV apps feels intuitive and responsive | Supports popular frontend frameworks like React, React Native, Next.js, and Electron, and offers a unified development platform that goes beyond spatial navigation | Primarily focused on providing spatial navigation capabilities using React Hooks |
GitHub activity | Has a dedicated GitHub presence with about 97 stars | Exhibits high activity on GitHub with a larger number of stars, suggesting a strong and active community | Shows a growing level of GitHub activity with about 264 stars |
Programming Language and Framework | TypeScript | TypeScript | TypeScript |
License | Not explicitly mentioned | MIT License | MIT License |
Developing a cross-platform TV app with React Native is increasingly important as apps for smart TVs continue to grow in popularity and relevance. The react-tv-space-navigation library offers a comprehensive solution for navigating the unique challenges of TV app development.
This approach not only simplifies the process of creating apps for various TV platforms like AndroidTV, tvOS, and web TV but also enhances the user experience with intuitive and accessible navigation.
The use of react-tv-space-navigation demonstrates how spatial navigation can be seamlessly integrated into React Native projects. This method greatly benefits both developers and users, ensuring that navigating through TV apps with remote control is as smooth and natural as possible.
You can find the source code for the application built in this tutorial in this GitHub repository.
For those looking to dive deeper into this subject, the React Native documentation and the react-tv-space-navigation GitHub repository are excellent resources. These platforms provide detailed insights and practical examples that can help developers effectively implement these techniques in their TV app projects.
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]