TV apps are more and more popular. And there are many different platforms: AndroidTV, AppleTV, and web. Each of these platforms has its own operating system and unique implementation APIs. The user experience also varies slightly across platforms due to the different types of remote control each utilizes.
React Native is a really good candidate to solve this challenge. It is an amazing solution for rendering mobile and web apps already. And it is just as amazing for TV apps!
One of the hardest challenges we had to solve while building a cross-platform TV app was the spatial navigation. Which component should be focused when I press right, down, right? It is not trivial.
Wikipedia : In computing, spatial navigation is the ability to navigate between focusable elements, such as hyperlinks and form controls, within a structured document or user interface according to the spatial location.
There are many solutions to this problem. There is now a native solution for both AndroidTV and tvOS that was upstreamed in react-native-tvos (many thanks to the contributors who did that). But it won’t work on the web. On the other hand, Norigin developed its own navigation system for the web with Norigin-Spatial-Navigation, but it does not work well on native apps.
We identified two ways of handling the spatial navigation: pixel-based navigation and declaration-based navigation.
Pixel-based navigation relies on visual layout of focusable elements. To determine the next element to focus, it compares distances to the nearest elements in the desired direction.
Declaration-based navigation relies on a structured declaration of spatial positions: we manually indicate that an element is above another using section-like parent navigation nodes, and the React rendering order.
We implemented a React library to solve these issues. You can check it out here: react-tv-space-navigation
As we can’t compute pixel positions easily on different platforms, pixel-based navigation cannot easily be cross-platform.
Thus we chose the second solution with our library: declaration-based navigation.
This is the most efficient way to ensure seamless cross-platform functionality. In addition to this, we have prioritized flexibility in UI-design, ensuring freedom to change layout as desired without hitting any dead-ends.
Space navigation declaration is powered by the great lib bbc/lrud. LRUD is a robust and UI-agnostic library of a spatial navigation representation. It is well tested and high quality code, congratulations to the author 👏.
Please note that it entered maintenance mode since they released a new version (which is not UI-agnostic anymore 😢), but we think it’s low risk on our end since it’s easily maintainable.
Our library is mostly a wrapper around LRUD (which holds all the navigation logic). It is made accessible through a React-friendly declarative API, ensuring ease of use and implementation in various applications.
One of our goals was to aim at simplicity. We love the declarative philosophy of React, and we want to keep it that way for a TV app. We want the spatial navigation to be both simple and flexible. We chose to declare spatial nodes using React components that structure the spatial layout and how they relate to each other when using a remote. We’ll get into more details below.
We want to avoid the developer worrying about the spatial navigation logic. We don’t want them to implement behaviours. We want them to simply declare layouts and let the lib do its job.
Here is an example of the lib usage:
We won’t get into too much details here. You can check our talk for a deeper understanding of our solution: https://www.youtube.com/watch?v=Asn1TmCH2b0
As we said, we use LRUD for the spatial layout logic. Here’s how it works:
Here’s what an LRUD declaration would look like as code, and the corresponding layout that it should represent.
Once we know how LRUD works, we need to synchronise it with React.
To do so, we declare our LRUD elements once our component is rendered for the first time, and update LRUD values when they need to be. That’s basically it.
Plot twist: we’re not using useEffect to declare the nodes, you can check the talk above to understand why the hell we would not stick to the standard.
It’s a simple and common thing, and there’s a little twist to achieve it with space navigation. You simply need to wrap your element with an additional spatial navigation node that’s never removed. It’s a simple fix, and you can have a look at the docs for a better understanding!
You need to know that we are not using the native focus. It can lead to weird behaviours in development mode. For example, react-native-tvos has an overlay for dev errors that uses native focus. In that case, we’ll have two parallel focuses at the same time. It’s annoying but for now you can simply remove the yellowbox feature. We might find a better solution in the future.
In production, we’ve never seen cases where this conflict becomes an issue because we don’t use any other native popups. But you need to be aware of this limitation because it might be an issue someday (if you have a native AndroidTV/tvOS popup coming up for some reason!).
The lib does not support accessibility features yet. It’s still work in progress. If you want to give us a hand, help would be appreciated 🙂
Spatial navigation is hard. There are many existing solutions to solve it, but none of them was satisfying enough regarding cross-platform apps.
With react-tv-space-navigation, we’re proposing an alternative which addresses this issue for any platform that runs React. You will love the API of the library, as it is 100% declarative and aims at abstracting the navigation logic away.
There are a few caveats that should know about (accessibility, the focus system not being the native one, conditional rendering), but it’s been working super well for us.
Also, feel free to have a look at the code! It’s one of the most interesting thing we ever had to build, and you might find exciting topics in there as well 🙂
We hope you’ll enjoy it!