Skip to main content

Distance Calculation Algorithm

The spatial navigation algorithm determines which focusable component to move to next by calculating a weighted distance score for every candidate in the pressed direction. The candidate with the lowest score wins.

How the Algorithm Works​

When the user presses an arrow key:

  1. Determine direction — The library identifies the axis (horizontal for left/right, vertical for up/down) and whether the direction is incremental (right, down) or decremental (left, up).

  2. Find candidates — All registered components whose leading edge is beyond the trailing edge of the current component in the pressed direction.

  3. Adjacent slice detection — For each candidate, the library checks whether it overlaps significantly with the current component along the perpendicular axis. A candidate that overlaps is called "adjacent". Adjacent candidates are strongly preferred (they receive a 5× lower weight on secondary distance).

  4. Score calculation — Each candidate receives a score:

    score = (primaryDistance × 5) + (secondaryDistance × weight)

    where weight = 1 for diagonal candidates, 0.2 for adjacent candidates
  5. Selection — The candidate with the lowest score is focused next.

This algorithm is inspired by the spatial navigation model developed for TV browser implementations.


distanceCalculationMethod​

The distanceCalculationMethod option in init() controls how the reference points are chosen when measuring distances.

'corners' (default)​

Uses two corners of the current component (the corners closest to the direction of travel) as the reference points.

  • Best for: Grids and rows with uniform-size items.
  • Behavior: Most precise directional matching; avoids jumping over items.
init({ distanceCalculationMethod: 'corners' });

'edges'​

Uses the full edge of the current component (the entire side in the direction of travel) as the reference.

  • Best for: Lists with items of varying heights or widths.
  • Behavior: More forgiving; easier to navigate between items that don't perfectly align.
init({ distanceCalculationMethod: 'edges' });

'center'​

Uses the center point of the current component as the reference.

  • Best for: Sparse layouts where items are spread far apart.
  • Behavior: Least precise directional matching; prefers the geometrically nearest center.
init({ distanceCalculationMethod: 'center' });

Choosing a Method​

Layout typeRecommended method
Uniform grid (all items same size)'corners'
Horizontal rows with varying card sizes'edges'
Sparse, scattered layout'center'
Mixed or complex layoutTry each and use visualDebug: true to inspect

customDistanceCalculationFunction​

Override the secondary-axis distance calculation with your own logic. This function is called for every candidate and replaces the built-in secondary distance calculation.

type CustomDistanceCalculationFunction = (
refCorners: Corners,
siblingCorners: Corners,
isVerticalDirection: boolean,
distanceCalculationMethod: string
) => number;

Corners type​

interface Corners {
nearPlumbLinePoint: { x: number; y: number };
farPlumbLinePoint: { x: number; y: number };
nearStraightLinePoint: { x: number; y: number };
farStraightLinePoint: { x: number; y: number };
}

Example: Bias toward items closer to the vertical center​

init({
distanceCalculationMethod: 'corners',
customDistanceCalculationFunction: (
refCorners,
siblingCorners,
isVerticalDirection
) => {
// Use the midpoint of each component
const refMidX =
(refCorners.nearPlumbLinePoint.x + refCorners.farPlumbLinePoint.x) / 2;
const sibMidX =
(siblingCorners.nearPlumbLinePoint.x +
siblingCorners.farPlumbLinePoint.x) /
2;
const refMidY =
(refCorners.nearStraightLinePoint.y + refCorners.farStraightLinePoint.y) /
2;
const sibMidY =
(siblingCorners.nearStraightLinePoint.y +
siblingCorners.farStraightLinePoint.y) /
2;

if (isVerticalDirection) {
return Math.abs(refMidX - sibMidX);
}
return Math.abs(refMidY - sibMidY);
}
});

The function overrides only the secondary-axis distance. The primary-axis distance (along the direction of travel) is always calculated by the library.


Debugging Navigation Decisions​

Enable visualDebug: true to see a canvas overlay that shows:

  • Bounding boxes of all registered components
  • The direction lines from the currently focused component
  • Which component was selected and why
init({
debug: true,
visualDebug: true,
distanceCalculationMethod: 'corners'
});

This makes it easy to understand why a particular element wins or loses the distance calculation. See Debugging for more.


Overriding the Algorithm​

When distance-based scoring isn't the right fit for a container — for example, a carousel that should always move by exactly one item — a nextFocusResolver lets that container supply its own navigation logic entirely, bypassing this algorithm. See Next Focus Resolver.