Spatial Navigation

Editor’s Draft,

This version:
http://wicg.github.io/spatial-navigation
Issue Tracking:
GitHub
Inline In Spec
Editors:
(LG Electronics)
Florian Rivoal (Invited Expert)

Abstract

This specification defines a general model for navigating the focus using the arrow keys, as well as related CSS and JavaScript features.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

The following features are at-risk, and may be dropped during the CR period:

“At-risk” is a W3C Process term-of-art, and does not necessarily imply that the feature is in danger of being dropped or delayed. It means that the WG believes the feature may have difficulty being interoperably implemented in a timely manner, and marking it as such allows the WG to drop the feature if necessary when transitioning to the Proposed Rec stage, without having to publish a new Candidate Rec without the feature first.

This specification is rather long. To make it easier to read and focus on a particular area, a few checkboxes are provided below. Checking them hides part of the specification. This is only meant as a reading aid, the specification remains the full document.




1. Introduction

This section is not normative.

Historically, most browsers have not offered features to let users move the focus directionally. Some, such as TV browsers, have enabled users to move the focus using the arrow keys out of necessity, since no other input mechanism is available on a typical TV remote control.

Others, have enabled different key combinations to control spatial navigation, such as pressing the Shift key together with arrow keys.

This ability to move around the page directionally is called spatial navigation (or spatnav for short).

Spatial navigation can be useful for a webpage built using a grid-like layout, or other predominantly non linear layouts. The figure below represents a photo gallery arranged in a grid layout. If the user presses the Tab key to move focus, they need to press the key many times to reach the desired element. Also, the grid layout may arrange the layout of elements independently of their source order. Therefore sequential navigation using the Tab key makes focus navigation unpredictable. In contrast, spatial navigation moves the focus among focusable elements depending on their position allowing it to address problems encountered with sequential navigation.

When elements are laid out in a grid pattern, spatial navigation makes it much easier to predict and control where focus should move to.
Application using a grid-like layout.

While arrow keys are naturally suited to control spatial navigation, no previous specification describes how that should work, or how it may be controlled. This specification introduces a processing model for spatial navigation, as well as APIs enabling authors to control and override how spatial navigation works.

Note: Some aspects of this specification, such as the JavaScript Events and APIs could also be extended to sequential navigation, in order to make sure that keyboard navigation in general has a consistent and well defined model.

Note: As a general principle, keyboard navigation, and spatial navigation in particular, should be possible to use and control without JavaScript, and declarative solutions are therefore preferred. Since spatial navigation depends on layout, that means CSS is typically the right mechanism to define spatial navigation related controls. However, in the spirit of the Extensible Web Manifesto [EXTENSIBLE], we feel it is important to provide the right JavaScript primitives to let authors experiment and explore the problem space. More declarative features may be added later, based on feedback and experience acquired through such JavaScript usage.

Note: A few features are marked at-risk. The editors of this specification believe they represent an important part of the user or author experience of the features defined in specification. At the same time, the core functionality of this specification can be implemented without implementing these so it seems possible that implementors may choose to down-prioritize them to reduce the scope of a first implementation. While it is hoped that these features will be implemented as well, they are marked at-risk in recognition that they might not be at first.

2. Module interaction

This document depends on the Infra Standard [infra].

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as described in RFC 2119. [RFC2119]

3. Overview

This section is not normative.

Using a UA-defined mechanism (typically arrow keys, possibly in combination with modifier keys like Shift or Control), the user may ask the User Agent to navigate in a particular direction. This will either move the focus from its current location to a new focusable item in the direction requested, or scroll if there is no appropriate item.

More specifically, the User Agent will first search for visible and focusable items in the direction indicated within the current spatial navigation focus container (by default the root element, scrollable elements, and iframes, but other elements can be made into spatial navigation focus containers using the spatial-navigation-contain property).

If it finds any, it will pick the best one for that direction, and move the focus there.

If it does not, it will scroll the spatial navigation focus container in the requested direction instead of moving focus. Doing so may uncover focusable elements which would then be eligible targets to move the focus to next time spatial navigation in the same direction is requested.

If the spatial navigation focus container cannot be scrolled, either because it is not a scrollable element or because it is already scrolled to the maximum in that direction, the User Agent will select the next spatial navigation focus container up the ancestry chain, and repeat the process of looking for eligible focus targets, selecting the best one if there’s any, scrolling if not, going up the ancestry chain if it cannot scroll, until it has either moved focus, scrolled, or reached the root.

Note: As a consequnce of this processing model, the elements that are reachable by sequential navigation and by spatial navigation are almost the same: elements that are currently outside of the viewport of a scrollable element can only be reached once they have been scrolled into view, and those that cannot be scrolled into view, for instance because they are placed at a negative offset, are out of reach.

At key points during this search for the appropriate response to the spatial navigation request, the User Agent will fire events. These enable authors to prevent the upcoming action (by calling preventDefault()), and if desired to provide an alternate action, such as using calling the focus() method on a different element of the author’s choosing.

To help authors write such alternate actions, and as part of exposing underlying platform primitives as per the Extensible Web principles, this specification also defines JavaScript APIs that expose key constructs of the underlying model.

See §6.2 Navigation Event Types for details about the various events, and §5 JavaScript API for details about the JavaScript API.

In this example, shows how a series of focusable elements arranged in a scrollable element would be navigated when using spatial navigation. For the sake of keeping the description simple, this example assumes a UA where spatial navigation is triggered using arrow keys.
Moving focus to the visible element in the spatial navigation focus container.

On the left of figure 2, "Box 2" is focused. Pressing the ArrowDown key moves focus to "Box 3" without scrolling because "Box 3" is visible in the scrollport of the spatial navigation focus container.

Moving focus to the hidden element in the spatial navigation focus container.

On the first of figure 3, under "Box 3", there isn’t any visible element in the scrollport. Therefore, the effect of pressing the ArrowDown is to scroll down, as shown in the second. The next press of the ArrowDown key makes "Box 4" come into the scrollport, and the focus will move to it when there is additional pressing the ArrowDown, as the fourth.

This example uses the markup as follows:

#scroller {
    width: 700px;
    height: 700px;
    overflow-x: hidden;
    overflow-y: auto;
}

.box {
    width: 150px;
    height: 110px;
    background-color: blue;
}

.box:focus {
    background-color: red;
}
<div id="scroller">
    <div class="box" tabindex="0">Box 1</div>
    <div class="box" tabindex="0">Box 2</div>
    <div class="box" tabindex="0">Box 3</div>
    <div class="box" tabindex="0">Box 4</div>
</div>

4. Triggering Spatial Navigation

When the user triggers spatial navigation in a given direction, the User Agent must run the spatial navigation steps in that direction.

This specification does not define what UI mechanism User Agents should offer to users to trigger spatial navigation. This intentionally left for User Agents to decide.

Note: It is expected that User Agents on devices with limited input capabilities, such as TVs operated with a remote control, or devices operated with a game controller, will use spatial navigation as their primary or exclusive navigation mechanism.

Although it is possible for User Agents to implement the processing model and APIs defined by the specification without giving any direct means to the user to trigger spatial navigation themselves, this specification recommends not to do so: User Agents should offer a means for users to trigger spatial navigation directly, without having to use the APIs.

Note: Conversely, authors should assume that spatial navigation may be triggered by the UA in response to user actions even if the author has not invoked any of the APIs.

Regardless of the actual mechanism chosen to trigger spatial navigation, the following requirements apply:

5. JavaScript API

5.1. Triggering Navigation Programmatically

This api is clearly useful for testing purposes. It may also be useful for authors, but the use cases are less strong. Maybe this should be moved to WebDriver and be a testing-only api. Maybe it is fine as is.

enum SpatialNavigationDirection {
    "up",
    "down",
    "left",
    "right",
};

partial interface Window {
    void navigate(SpatialNavigationDirection dir);
};
The navigate() method must follow these steps:

If dir is "up", "down", "left", or "right", run the spatial navigation steps in direction dir.

5.2. Low level APIs

Note: These APIs are designed to be low level constructs following the processing model closely. As such, they should be easy to use by authors who want to extend or override the way spatial navigation works.

enum FocusableAreaSearchMode {
    "visible",
    "all"
};

dictionary FocusableAreasOptions {
    FocusableAreaSearchMode mode;
};

dictionary SpatNavSearchOptions {
    required SpatialNavigationDirection dir;
    sequence<Node>? candidates;
    Node? container;
};

partial interface Element {
    Node getSpatnavContainer();
    sequence<Node> focusableAreas(optional FocusableAreasOptions arg);
    Node? spatNavSearch(SpatNavSearchOptions arg);
};

Note: The way the direction is expressed allows us to expand to more than 4-way navigation later of if this is found necessary. More directional keywords or a numerical angle could be added.

Note: the focusableAreas() and getSpatnavContainer() methods are at-risk.

The getSpatnavContainer() method must follow these steps:
  1. Return the element if it is a spatnav container, or the nearest ancestor of the element that is a spatnav container, or the document if the nearest spatnav container is the viewport.

The focusableAreas() method must follow these steps:
  1. Let v be false if the argument’s mode attribute if present and equal to "all", or true otherwise.

  2. Let areas be the result of finding focusable areas within the element with the visibleOnly argument set to v

  3. Let anchors be a clone of areas, with every focusable area which is not itself a Node replaced with its DOM anchor.

  4. Return anchors

The spatNavSearch() method must follow these steps:
  1. Let d be the argument’s dir attribute

  2. If the argument’s candidates attribute is not null, then let areas be that attribute, else, let areas be the result of finding focusable areas within the argument’s container attribute is not null, or the element’s nearest spatnav container ancestor

  3. Return the result of selecting the best candidate within areas in direction d from the element

Note: When neither a container nor a list of candidates is provided, this only searches through the visible focusable areas of the nearest spatnav container ancestor. If none are found, this does not climb further up the ancestry chain, and the result will be null.

The following code changes the behavior of spatial navigation from scrolling when there is no focusable element visible, to jumping to focusable elements even when they are not visible.
document.addEventListener("navbeforescroll", function(e) {
    var container = e.relatedTarget;
    var areas = container.focusableAreas({ mode: "all" });

    if (areas.length == 0)) { return; }

    e.preventDefault();
    var t = e.target.spatNavSearch({
        dir: e.dir,
        candidates: areas
    });
    t.focus();
});
The following code changes the behavior of spatial navigation so that when a scroll container would get focused, if it has at least one visible focusable descendant, the focus is automatically transfered to it.
document.addEventListener("navbeforefocus", function(e) {
    e.preventDefault();

    if (e.dir === "forward" || e.dir === "backward" ) {
        e.dir="top";
    }

    var t = e.relatedTarget;
    while (t.isSameNode(t.getSpatnavContainer())) {
        var areas = t.focusableAreas();

        if (areas.length == 0)) { break; }

        t = t.spatNavSearch({
            dir:  e.dir,
            candidates: areas
        });
    }
    t.focus();
});
The following code changes the behavior of spatial navigation to trap the focus within a spatnav container: when no further focusable elements can be found in the requested direcition and the spatnav container cannot be scrolled any futher, we loop back to the other side instead of searching outside of it, either by focusing or scrolling depending on what is available.

The focus can still be moved outside by sequential navigation, mouse interaction, programatic calls to focus()

document.addEventListener("navnotarget", function(e) {
    e.preventDefault();

    var c = e.relatedTarget;
    var areas = c.focusableAreas({mode: "all"});

    if (areas.length == 0)) {
        switch(e.dir) {
            case "down":
                c.scrollTop=0;
                    break;
            case "up":
                c.scrollTop=c.scrollHeight;
                    break;
            case "right":
                c.scrollLeft=0;
                    break;
            case "left":
                c.scrollLeft=c.scrollWidth;
                    break;
        }
    } else {
        var t = c.spatNavSearch({
            dir: e.dir,
            candidates: areas
        });
        t.focus();
});

6. Navigation Events

6.1. Interface NavigationEvent

The NavigationEvent interface provides specific contextual information associated with spatial navigation.

To create an instance of the NavigationEvent interface, use the NavigationEvent constructor, passing an optional NavigationEventInit dictionary.

[Constructor(DOMString type, optional NavigationEventInit eventInitDict)]
interface NavigationEvent : UIEvent {
      readonly attribute SpatialNavigationDirection dir;
      readonly attribute EventTarget? relatedTarget;
};

dictionary NavigationEventInit : UIEventInit {
      required SpatialNavigationDirection dir;
      EventTarget? relatedTarget = null;
};

6.2. Navigation Event Types

This section and its subsections are not normative.

The Navigation event types are summarized below. For full normative details, see §7 Processing Model.

6.2.1. navbeforefocus

The navbeforefocus event occurs before spatial navigation changes the focus.

Type navbeforefocus
Interface NavigationEvent
Bubbles Yes
Cancelable Yes
Attributes of the event
NavigationEvent.relatedTarget
The DOM anchor of the focusable area that will be focused
NavigationEvent.dir
The direction of the navigation as requested by the user
This example shows the UI Events §event-order when pressing the ArrowRight key. For the sake of keeping the description simple, this example assumes a UA where spatial navigation is triggered using arrow keys.
Event type KeyboardEvent.key Notes
1 keydown ArrowRight MUST be a key which can activate spatial navigation, such as the arrow keys, or spatial navigation is not activated.
2 navbeforefocus Sent if the candidates for spatial navigation is not null, or this is not generated.
3 focusin Sent before the target element receives focus.
4 focus Sent after the target element receives focus.

6.2.2. navbeforescroll

The navbeforescroll event occurs before spatial navigation triggers scrolling.

Type navbeforescroll
Interface NavigationEvent
Bubbles Yes
Cancelable Yes
Attributes of the event
NavigationEvent.relatedTarget
The element that will be scrolled if the event is not canceled
NavigationEvent.dir
The direction of the navigation as requested by the user
This example shows the UI Events §event-order when pressing the ArrowDown key in the situation like the following figure. For the sake of keeping the description simple, this example assumes a UA where spatial navigation is triggered using arrow keys.
An image about navbeforescroll
"Box 2" gains the focus and there isn’t any candidate in a downward direction in the scrollport.
Event type Event target relatedTarget Notes
1 keydown #box2 N/A MUST be a key which can activate spatial navigation, such as the arrow keys, otherwise spatial navigation is not triggered.
2 navbeforescroll #box2 #scrollContainer Sent if #scrollContainer doesn’t contain any candidate in the scrollport, otherwise this would not be generated.

After navbeforescroll is fired, pressing the ArrowDown key triggers scrolling down the scrollbar like in the figure below:

An image of the result about navnotarget
The result of moving focus when there isn’t any candidate in the scrollport.

This example uses the markup as follows:

#scrollContainer {
    width: 700px;
    height: 700px;
    overflow-x: hidden;
    overflow-y: auto;
}

.item {
    width: 150px;
    height: 110px;
    background-color: blue;
}

.item:focus {
    background-color: red;
}
<div id="scrollContainer">
    <div id="box1" class="item" tabindex="0">Box 1</div>
    <div id="box2" class="item" tabindex="0">Box 2</div>
    <div id="box3" class="item" tabindex="0">Box 3</div>
</div>

6.2.3. navnotarget

The navnotarget event occurs before going up the tree to search candidates in the nearest ancestor spatnav container when spatial navigation has failed to find any candidate within the current spatnav container.

If the spatnav container is scrollable, the event occurs when there isn’t any candidate in it and it cannot be scrolled at the same time.

Type navnotarget
Interface NavigationEvent
Bubbles Yes
Cancelable Yes
Attributes of the event
NavigationEvent.relatedTarget
The spatnav container that was searched in.
NavigationEvent.dir
The direction of the navigation as requested by the user
This example shows the UI Events §event-order when pressing the ArrowDown key in the situation like the following figure. For the sake of keeping the description simple, this example assumes a UA where spatial navigation is triggered using arrow keys.
An image about navnotarget
Moving focus when there isn’t any candidate in the scroll container.
Event type Event target relatedTarget Notes
1 keydown #box2 N/A MUST be a key which can activate spatial navigation, such as the arrow keys, otherwise spatial navigation is not triggered.
2 navnotarget #box2 #scrollContainer Sent if #scrollContainer doesn’t contain any candidate and cannot be scrolled, otherwise this would not be generated.
3 navbeforefocus #box2 #box3 Sent if the candidates in #container is not null, otherwise this would not be fired.
4 focusin #box3 N/A Sent before the target element receives focus.
5 focus #box3 N/A Sent after the target element receives focus.

The result of this example is the figure as follows:

An image of the result about navnotarget
The result of moving focus when there isn’t any candidate in the scrollport and scroll container cannot be scrolled.

This example uses the markup as follows:

#container {
    width: 900px;
    height: 1400px;
}

#scrollContainer {
    width: 700px;
    height: 700px;
    overflow-x: hidden;
    overflow-y: auto;
}

.item {
    width: 150px;
    height: 110px;
    background-color: blue;
}

.item:focus {
    background-color: red;
}
<div id="container">
    <div id="scrollContainer">
        <div id="box1" class="item" tabindex="0">Box 1</div>
        <div id="box2" class="item" tabindex="0">Box 2</div>
    </div>
    <div id="box3" class="item" tabindex="0">Box 3</div>
</div>

7. Processing Model

The §3 Overview section gives a high level idea of how spatial navigation works, to help readers of this specification build a general mental model. It uses intuitive but imprecise terminology, and glosses over many details for the sake of readability.

This section defines the corresponding normative behavior and aims for as much detail as necessary to fully define the behavior.

The following currently does not account for the proposed overscroll-behavior specification. <https://github.com/wicg/spatial-navigation/issues/19>

The following does not take shadow dom into account. <https://github.com/wicg/spatial-navigation/issues/21>

7.1. Groupings of elements

While the general model for spatial navigation is to work from the layout of the document and the relative position of focusable elements, the User Agent is required to prioritize finding elements from a local logical grouping, only looking for focusable elements outside of the grouping if a suitable one cannot be found inside it (see §7.2 Navigation for details).

Such groupings are called spatial navigation focus containers (or spatnav containers for short).

By default, spatnav containers are established by:

Additional spatnav containers can be created using the spatial-navigation-contain property (see §8.1 Creating additional spatnav containers: the spatial-navigation-contain property).
This figure is not normative. It gives an overview of the processing model further defined in this section.

There can be a spatial navigation starting point. It is initially unset. The user agent may set it when the user indicates that it should be moved.

Note: For example, the user agent could set it to the position of the user’s click if the user clicks on the document contents, and unset when the focus is moved (by spatial navigation or any other means).

If the UA sets both a spatial navigation starting point and a sequential focus navigation starting point, they must not be set differently.

The focusing steps should probably reset the spatial navigation starting point <https://github.com/wicg/spatial-navigation/issues/23>

To run the spatial navigation steps in direction, do the following:
  1. Let startingPoint be the DOM anchor of the currently focused area of a top-level browsing context.

  2. If the spatial navigation starting point is not null and it is inside startingPoint then set startingPoint to the spatial navigation starting point

    • If startingPoint is an node, let eventTarget be startingPoint

    • else (assert: the starting point is a position) let eventTarget be the node which contains startingPoint

  3. If eventTarget is the Document or the document element, set eventTarget be the body element if it is not null or to the document element otherwise.

  4. If startingPoint is either a scroll container or the document

    1. Let candidates be the result of finding focusable areas within startingPoint

  5. Let container be the nearest ancestor of eventTarget that is a spatnav container.

  6. Loop: Let candidates be the result of finding focusable areas within container, excluding startingPoint

  7. If candidates is empty:

  8. Let bestCandidate be the result of selecting the best candidate within candidates in direction starting from startingPoint

  9. Fire an event named navbeforefocus at eventTarget using NavigationEvent with its dir set to direction and relatedTarget set to bestCandidate and with it’s bubbles and cancelable attributes set to true, and return if the result is false

  10. Run the focusing steps for bestCandidate and return

7.3. Focus Navigation Heuristics

Note: The following algorithms are inspired from Chrome’s implementation as well as from the old WICD Spec. Implementors who find better approaches or refinements to these approaches are strongly encouraged to provide feedback and help improve this specification in order to maximize interoperability. In particular, divergences in how User Agents find focusable areas may cause some elements to be focusable in some User Agents but not in others, which would be bad for users.

All geometrical operations in this section are defined to work on the result of CSS layout, including all graphical transformations, such as relative positioning or [CSS-TRANSFORMS-1].

The boundary box of an object is defined as follows:

CSS should have a term for “border box taking into account corner shaping properties like border-radius”. <https://github.com/w3c/csswg-drafts/issues/2324>

To find focusable areas within a containing element C, with an optional visibleOnly argument that defaults to true, follow the following steps:

  1. Let focusables be the set of all the focusable areas that are descendants of C.

  2. The UA should remove from focusables elements whose tabindex attribute is set to a negative value.

    Note: This is a "SHOULD" in order to mirror the exclusion of elements with negative tabindex from the sequential focus navigation order as defined in HTML Standard §the-tabindex-attribute.

  3. If visibleOnly is false, return focusables.

    Note: focusables may be empty

  4. Let insideArea be

  5. Let visibles be the subset of items in focusables whose boundary box is at least partly within insideArea.

    Except for elements that are in the currently non visible part of a scroller, spatial navigation does not automatically exclude elements which cannot be clicked on, for example due to being obscured by some other element. To avoid breaking assumptions in the application logic if a user actually focuses and activates such an element, and to avoid confusing users by focusing invisible or apparently unreachable elements, authors should use make these elements unreachable to spatial navigation using the same best practices as for making elements unreachable to sequential navigation, such as using tab-index="-1" or the inert attribute.
  6. Return visibles.

    Note: visibles may be empty

To select the best candidate within a set of candidates in a direction dir, starting from starting point, follow the following steps:

  1. If candidates is empty, return null

  2. If candidates contains a single item, return that item

  3. Let insideArea be

  4. Let insiders be the subset of candidates items who are descendants of starting point and whose boundary box’s

    • top edge is below the top edge of insideArea if D is down

    • bottom edge is above the bottom edge of insideArea if D is up

    • right edge is left of the right edge of insideArea if D is left

    • left edge is right of the left edge of insideArea if D is right

    Note: this sub-setting is necessary to avoid going in the opposite direction than the one requested.

    • If insiders is non empty

      1. Let closest subset be the subset of insiders whose boundary box’s

        • top edge is closest to the top edge of insideArea if D is down

        • bottom edge is closest to the bottom edge of insideArea if D is up

        • right edge is closest to the right edge of insideArea if D is left

        • left edge is closest to the left edge of insideArea if D is right

      2. If closest subset contains a single item, return that item, else return the first item of closest subset in document order

    • Else

      1. Set candidates be the subset of its items whose boundary box’s geometric center is within the closed half plane whose boundary goes through the geometric center of the starting point and is perpendicular to D.

      2. For each candidate in candidates, find the points P1 inside the boundary box of starting point and P2 inside the boundary box of candidate that minimize the distance between these two points, when distance is defined as follows:

        distance:
        A + B + C - D
        A:
        The euclidian distance between P1 and P2
        B:
        The absolute distance in the dir direction between P1 and P2
        C:
        The absolute distance in the direction which is orthogonal to dir between P1 and P2
        D:
        The square root of the area of intersection between the boundary boxes of candidate and starting point
      3. Return the item of the candidates set that has the smallest distance.

8. Controlling spatial navigation through declarative means

8.1. Creating additional spatnav containers: the spatial-navigation-contain property

Name: spatial-navigation-contain
Value: auto | contain
Initial: auto
Applies to: all elements
Inherited: no
Percentages: n/a
Computed value: as specified
Canonical order: per grammar
Animation type: discrete
auto
If the element is a scroll container then it establishes a spatial navigation focus container, otherwise it does not.
contain
The element establishes a spatial navigation focus container

Note: In addition, as per §7.1 Groupings of elements, the viewport of a browsing context (not limited to the top-level browsing context) also establishes a spatial navigation focus container.

The following example shows a simplified TV program schedule or calendar. It has a grid of elements representing TV shows or calendar entries, and some UI buttons around it.

In this case, the grid is quite sparse, so if the user tries to move down from "Foo", focus would be moved to "Next Week", as it is objectively closer in the down direction. The same is true for going down from "Bar": the focus would be moved to "Previous Week".

M T W T F S S
0-6 Foo
6-9 Bar
9-12
12-18
18-21
21-24 Baz
<div>
    <button>Previous Week</button>
    <table>
        <tr><td><th>M<th>T<th>W<th>T<th>F<th>S<th>S
        <tr><td>0-6<td><td><td><td><td><td><td><a href="#">Foo</a>
        <tr><td>6-9<td><a href="#">Bar</a><td><td><td><td><td><td>
        <tr><td>9-12<td><td><td><td><td><td><td>
        <tr><td>12-18<td><td><td><td><td><td><td>
        <tr><td>18-21<td><td><td><td><td><td><td>
        <tr><td>21-24<td><td><td><td><td><td><a href="#">Baz</a><td>
    </table>
    <button>Next Week</button>
</div>
table, td, th {
    border-collapse: collapse;
    border: solid 1px;
}
td { width: 12.5%; }
div {
    display: grid;
    grid-template-columns: auto 1fr auto;
}
button { align-self: center; }

Because the elements in the table are semantically related to eachother, the author may want to provide a different navigation experience giving priority to movements inside the grid once you have focused one of its items.

Adding table { spatial-navigation-contain: contain; } to the stylesheet would result it this behavior. It would still be possible to move the focus out of the table, for example by going right from "Foo". Since there is nothing in the grid that is to the right, the focus would move to "Next week".

However, if the user navigates down from "Foo", there is something inside the grid, so focus will move there without considering things that are outside.

Note: the spatial-navigation-contain property is at-risk.

Appendix A. Scroll extensions

This section proposes a few extensions to CSS that should be integrated in upstream specifications, but are hosted here until then.

Terminology like this should be in [CSSOM-VIEW-1], [CSS-OVERFLOW-3], [CSS-SCROLL-SNAP-1]. <https://github.com/w3c/csswg-drafts/issues/2322>

An element e can be manually scrolled in a given direction d if:

[CSSOM-VIEW-1] should probably define how to perform a scroll in a given direction without an explicit position. Until then, we roll our own. <https://github.com/w3c/csswg-drafts/issues/2323>

To directionally scroll an element e in direction dir:

  1. Let d be a User Agent defined distance.

  2. Let x be e’s current scroll position on the x axis.

  3. Let y be e’s current scroll position on the y axis.

  4. Use the scroll an element algorithm from [CSSOM-VIEW-1] on e to

    • (x, y - d) if dir is up

    • (x, y + d) if dir is down

    • (x - d, y) if dir is left

    • (x + d, y) if dir is right

Acknowledgements

The editors of this specification would like to thank the following individuals for their feedback and contributions (in alphabetical order):

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-DISPLAY-3]
Tab Atkins Jr.; Elika Etemad. CSS Display Module Level 3. 20 April 2018. WD. URL: https://www.w3.org/TR/css-display-3/
[CSS-OVERFLOW-3]
David Baron; Florian Rivoal. CSS Overflow Module Level 3. 31 May 2016. WD. URL: https://www.w3.org/TR/css-overflow-3/
[CSS-POSITION-3]
Rossen Atanassov; Arron Eicholz. CSS Positioned Layout Module Level 3. 17 May 2016. WD. URL: https://www.w3.org/TR/css-position-3/
[CSS-SCROLL-SNAP-1]
Matt Rakow; et al. CSS Scroll Snap Module Level 1. 14 December 2017. CR. URL: https://www.w3.org/TR/css-scroll-snap-1/
[CSS-VALUES-4]
CSS Values and Units Module Level 4 URL: https://drafts.csswg.org/css-values-4/
[CSS2]
Bert Bos; et al. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. 7 June 2011. REC. URL: https://www.w3.org/TR/CSS2/
[CSSOM-VIEW-1]
Simon Pieters. CSSOM View Module. 17 March 2016. WD. URL: https://www.w3.org/TR/cssom-view-1/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[UIEVENTS]
Gary Kacmarcik; Travis Leithead. UI Events. 4 August 2016. WD. URL: https://www.w3.org/TR/uievents/
[WebIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

Informative References

[CSS-TRANSFORMS-1]
Simon Fraser; et al. CSS Transforms Module Level 1. 30 November 2017. WD. URL: https://www.w3.org/TR/css-transforms-1/
[EXTENSIBLE]
The Extensible Web Manifesto. 10 June 2013. URL: https://extensiblewebmanifesto.org/

Property Index

Name Value Initial Applies to Inh. %ages Anim­ation type Canonical order Com­puted value
spatial-navigation-contain auto | contain auto all elements no n/a discrete per grammar as specified

IDL Index

enum SpatialNavigationDirection {
    "up",
    "down",
    "left",
    "right",
};

partial interface Window {
    void navigate(SpatialNavigationDirection dir);
};

enum FocusableAreaSearchMode {
    "visible",
    "all"
};

dictionary FocusableAreasOptions {
    FocusableAreaSearchMode mode;
};

dictionary SpatNavSearchOptions {
    required SpatialNavigationDirection dir;
    sequence<Node>? candidates;
    Node? container;
};

partial interface Element {
    Node getSpatnavContainer();
    sequence<Node> focusableAreas(optional FocusableAreasOptions arg);
    Node? spatNavSearch(SpatNavSearchOptions arg);
};

[Constructor(DOMString type, optional NavigationEventInit eventInitDict)]
interface NavigationEvent : UIEvent {
      readonly attribute SpatialNavigationDirection dir;
      readonly attribute EventTarget? relatedTarget;
};

dictionary NavigationEventInit : UIEventInit {
      required SpatialNavigationDirection dir;
      EventTarget? relatedTarget = null;
};

Issues Index

This api is clearly useful for testing purposes. It may also be useful for authors, but the use cases are less strong. Maybe this should be moved to WebDriver and be a testing-only api. Maybe it is fine as is.
The following currently does not account for the proposed overscroll-behavior specification. <https://github.com/wicg/spatial-navigation/issues/19>
The following does not take shadow dom into account. <https://github.com/wicg/spatial-navigation/issues/21>
The focusing steps should probably reset the spatial navigation starting point <https://github.com/wicg/spatial-navigation/issues/23>
is it sane from a security standpoint that this propagates up from iframes? <https://github.com/wicg/spatial-navigation/issues/28>
CSS should have a term for “border box taking into account corner shaping properties like border-radius”. <https://github.com/w3c/csswg-drafts/issues/2324>
Terminology like this should be in [CSSOM-VIEW-1], [CSS-OVERFLOW-3], [CSS-SCROLL-SNAP-1]. <https://github.com/w3c/csswg-drafts/issues/2322>
[CSSOM-VIEW-1] should probably define how to perform a scroll in a given direction without an explicit position. Until then, we roll our own. <https://github.com/w3c/csswg-drafts/issues/2323>