Skip to main content

Navigation

Living Apps Core SDK provides a navigation framework that will make it easier to focus DOM elements using remote control

Add .la-navigable class to those elements you want to be navigable and set the data attributes

  • data-up
  • data-right
  • data-left
  • data-down

to define how the focus will change when the user presses right/left/up/down while a DOM element is focused.

note

data-{up|right|down|left} attribute should be the id of another DOM element

Then, you can use .la-navigable:focus selector to define the style for the focused element:

.la-navigable:focus {
border: 8px solid MediumVioletRed;
}

This React App:

export function App() {
return (
<>
<h1>la-navigable</h1>
<main>
<div
id="elem-1"
className="la-navigable"
data-right="elem-2"
data-left="elem-3"
>
#1
</div>
<div
id="elem-2"
className="la-navigable"
data-right="elem-3"
data-left="elem-1"
>
#2
</div>
<div
id="elem-3"
className="la-navigable"
data-right="elem-1"
data-left="elem-2"
>
#3
</div>
</main>
</>
);
}

Renders the following:

la-navigable raw gif

Focus

use laSdk.focus() and laSdk.focusedElement to focus DOM elements programmatically.

This React App:

export function App() {
const ref = useRef(null);

useEffect(() => {
if (ref.current) {
laSdk.focus(ref.current);
}
}, []);

return (
<>
<h1>laSdk.focus()</h1>
<main>
<div className="la-navigable">#1</div>
<div className="la-navigable">#2</div>
<div ref={ref} className="la-navigable">
Get focus
</div>
</main>
</>
);
}

Renders the following:

lasdk-focus example

Scrollable: rows & columns

Unfortunately, the STB browser does not support Element.scrollIntoView() method. However, the Living Apps SDK Navigation framework provides an easy module to create scroll experiences in your navigable elements.

la-scrollable is a tool for creating standard navigation with rows and columns, similar to what you're accustomed to seeing in TV apps. Check out our live demo for HTML examples with various navigation experiences.

lasdk-focus example

.la-scrollable

Add .la-scrollable class to the parent element of your .la-navigable child elements, the SDK will do the rest.

NOTE: Elements with la-scrollable class must not have position: absolute

tip

The data-la-scrollable-offset attribute can apply some extra space (in px) to the scroll.

<div
id="parent"
class="row"
class="la-scrollable"
data-la-scrollable-offset="48"
>
<div id="elem-1" class="item la-navigable" data-right="elem-2">Element 1</div>
<div id="elem-2" class="item la-navigable" data-left="elem-1">Element 2</div>
</div>
.row {
width: 200px;
display: flex;
overflow: hidden;
}

.item {
flex-shrink: 0;
}

.la-scrollable-fixed

By adding .la-scrollable-fixed class to your .la-scrollable element, the scroll behavior ensures that the focused element remains completely fixed in place.

<div id="parent" class="la-scrollable la-scrollable-fixed">
<div id="elem-1" class="la-navigable" data-right="elem-2">Element 1</div>
<div id="elem-2" class="la-navigable" data-left="elem-1">Element 2</div>
<div style="width: 200px; height: 1px; visibility: hidden"></div>
</div>
note

Include an additional non-navigable item child to occupy the extra space, ensuring that the last elements maintain their fixed scrolling behavior.

<div style="width: 200px; height: 1px; visibility: hidden"></div>

.la-scrollable-animated

The .la-scrollable-animated class enables a smooth scrolling effect. You can customize the animation speed by adding the data-la-scrollable-animation-step attribute (default is 30).

<div id="parent" class="la-scrollable la-scrollable-animated">
<div id="elem-1" class="item la-navigable" data-right="elem-2">Element 1</div>
<div id="elem-2" class="item la-navigable" data-left="elem-1">Element 2</div>
</div>

The la-scrollable-animating class is applied to the scrollable element during animation. This class can be utilized to enhance user experience, such as hiding the focus border while the animation is in progress:

.la-scrollable-animating .item {
border-color: transparent;
transition: border-color 0s;
}

.item:focus {
border-color: white;
transition: border-color 0.5s;
}

.la-scrollable-lazy

When dealing with a large number of scrollable items, it's crucial to prioritize performance by loading only the items that fit on the screen. Thanks to .la-scrollable-lazy class, the SDK automatically detects additions or removals of children elements and adjusts the scroll position accordingly.

Example: a thousand items row using React.js

const list = new Array(1_000).fill(null); // huge list

function ScrollableLazyDemo() {
// store current index on the state to re-render while navigating.
const [currentItemIndex, setCurrentItemIndex] = useState(0);

return (
<div
className="la-scrollable la-scrollable-lazy"
onScroll={(event) => {
// don't change the currentItemIndex state until the scroll ends

const isBeingAnimated = event.currentTarget.classList.contains(
'la-scrollable-animating',
);

if (!isBeingAnimated) {
// extract item index from data-index attribute
const index = laSdk.currentFocus?.dataset.index;

if (index) {
setCurrentItemIndex(+index);
}
}
}}
>
{list.map((_, i) =>
{/* render only 10 items on each side */}
Math.abs(currentItemIndex - i) > 10 ? null : (
<div
key={i}
data-index={i} {/* store item index */}
style={{ marginRight: '10px' }}
id={`item-1-${i}`}
className="la-navigable"
data-right={`item-1-${i + 1}`}
data-left={`item-1-${i - 1}`}
>
{i}
</div>
),
)}
</div>
);
}

Example: a thousand items grid using React.js

const VISIBLE_ROWS = 10
const COLUMNS = 5
const list = new Array(1_000).fill(null); // huge list

function getRowIndexFromItemIndex(i) {
return Math.floor(i / COLUMNS)
}

function GridDemo() {
// store current index on state to re-render while navigating.
const [currentRowIndex, setCurrentRowIndex] = useState(0);

return (
<div
className="la-scrollable la-scrollable-lazy"
style={{
width: '1920px'
height: '600px',
display: 'grid',
gridTemplateColumns: `repeat(${COLUMNS}, 300px)`
}}
onScroll={(event) => {
// don't change currentItemIndex state until scroll ends

const isBeingAnimated = event.currentTarget.classList.contains(
'la-scrollable-animating',
);

if (!isBeingAnimated) {
// extract item index from data-index attribute
const index = laSdk.currentFocus?.dataset.index;

if (index) {
const rowIndex = getRowIndexFromItemIndex(+index)
setCurrentRowIndex(rowIndex);
}
}
}}
>
{list.map((_, i) =>
{/* render only 10 items on each side */}
Math.abs(currentRowIndex - getRowIndexFromItemIndex(i)) > VISIBLE_ROWS ? null : (
<div
key={i}
data-index={i} {/* store item index */}
id={`item-${i}`}
className="la-navigable"
data-down={`item-${i + COLUMNS}`}
data-up={`item-${i - COLUMNS}`}
data-right={`item-${i + 1}`}
data-left={`item-${i - 1}`}
>
{i}
</div>
),
)}
</div>
);
}

Combining options

Use | to provide multiple options for data-<direction> attributes: data-up=id1|id2|id3|id4. The navigation framework will focus on the first element found on DOM.

<div id="element" class="la-navigable" data-proxy="another-element"></div>

When element gets focused, it'll proxy focus to another-element directly.

Events

Use onfocus and onblur event handlers to code your custom logic when a DOM element gets focused or blurred.

React Example:

<div
id="main-button"
class="la-navigable"
onFocus={(ev) => {
console.log('focus!')
}}
onBlur={(ev) => {
console.log('blur!)
}}
>
Focus me!
</div>