Chris Holt
Authors create many interactive control patterns that expect or can benefit from keyboard-accessible behavior such as arrow-key navigation.
This proposal enables authors to add a set of built-in keyboard behaviors for moving focus among a list or grid of focusable elements without having to write common boilerplate code (e.g., roving tabindex) to do so in their custom controls.
Authors may use the proposed focusgroup
HTML attribute (or related CSS properties) to declare that a subtree of focusable elements will get:
By standardizing focusgroup
, authors can leverage these behaviors in control patterns to provide users with keyboard consistency, default accessibility, and interoperability over existing solutions.
While this document emphasizes the usage of the keyboard arrow keys for accessibility navigation, we acknowledge that there are other input modalities that work (or can be adapted to work) equally well for focusgroup navigation behavior (e.g., game controllers, gesture recognizers, touch-based ATs, etc.)
focusgroup
enables authors to provide keyboard roving tabindex with almost no code. Compare the web platform’s native radio button group with a custom control using the radio group pattern.
A platform native radio group:
<p>Choose your pet:</p>
<div id=radiogroup>
<label>Dog <input type=radio name=mygroup value=dog></label>
<label>Cat <input type=radio name=mygroup value=cat></label>
<label>Turtle <input type=radio name=mygroup value=turtle></label>
</div>
A custom radio group using focusgroup
:
<p id=label>Choose your pet:</p>
<div role=radiogroup focusgroup onfocusin="/*update aria-checked values*/" aria-labelledby=label>
<span role=radio aria-checked=false tabindex=-1>Dog</span>
<span role=radio aria-checked=false tabindex=-1>Cat</span>
<span role=radio aria-checked=false tabindex=-1>Turtle</span>
</div>
What to notice:
focusgroup
enables down/right arrow keys to advance the focus to the next focusable element in DOM order (up/left keys will move the focus in reverse) just like in the native radio group.tabindex
is still required to make the <span>
elements focusable (the focusgroup
attribute doesn’t make any elements focusable by default, including the <div>
it’s declared on).tabindex
values are set to “-1” and no code has to update those values. focusgroup
can set keyboard focus on elements declared focusable via tabindex="-1"
via arrow keys. tabindex="-1"
still prevents these elements from being part of sequential focus navigation (e.g., the “tab sequence”)—with one exception: focusgroup
provides a special behavior that enables one focusable item within the group to get keyboard focus via sequential focus navigation even if all the focusable elements within are opting out of sequential focus navigation (i.e., all “focusables” are setting tabindex=-1
).onfocusin
event handler is present because focusgroup
does not manage selection state (and native radio groups do have selection linked to focus).focusgroup
example, focus will stop on the last focusable element. (But there’s an option to turn on wrapping if desired.)In this next example, the author is using a tab control pattern where the tab activation behavior is decoupled from selection (“manual tab activation”):
<div role=tablist focusgroup="inline wrap no-memory" aria-label="Common Operating Systems">
<button id=tab-1 type=button role=tab aria-selected=false aria-controls=tabpanel-1 tabindex=-1>Mac</button>
<button id=tab-2 type=button role=tab aria-selected=true aria-controls=tabpanel-2 tabindex=0>Windows</button>
<button id=tab-3 type=button role=tab aria-selected=false aria-controls=tabpanel-3 tabindex=-1>Linux</button>
</div>
<div id=tabpanel-1 role=tabpanel tabindex=0 aria-labelledby=tab-1 hidden> … </div>
<div id=tabpanel-2 role=tabpanel tabindex=0 aria-labelledby=tab-2> … </div>
<div id=tabpanel-3 role=tabpanel tabindex=0 aria-labelledby=tab-3 hidden> … </div>
What to notice:
focusgroup
is respected. When entering the focusgroup
, focus will always go to the first selected tab (with tabindex=0
). The no-memory
value prevents the focusgroup from remembering the last focused tab so that focus will always go to the selected tab on reentrance regardless of which element was focused last.tab-1
, then pressing the tab key moves focus to the selected tab, which is next in sequential focus navigation order. Pressing tab again moves the focus to tabpanel-2
which is next in sequential focus navigation order (because the other role=tabpanel
s are hidden
).wrap
from one end of the tablist to the other because of focusgroup=wrap
attribute value.focusgroup=inline
which restricts the axis of movement to keyboard directional arrow keys in the role=tablist
’s inline direction (assuming the <div>
’s writing-mode
is horizontal-tb
).aria-selected
values, the hidden
state of the controlled role=tabpanel
and the tabindex
values of tabs such that the newly selected role=tab
element is tabindex=0
while all others are tabindex=-1
.In a third example, the author is creating a navigation menubar. Both menuitem
s in the menubar
(“About” and “Admissions”) have popover menu
s. The “Admissions” menu has an additional submenu under “Tuition”.
<ul role=menubar aria-label="Mythical University" focusgroup="inline wrap">
<li role=none>
<a role=menuitem popovertarget=aboutpop href="…" tabindex=-1 title="">About</a>
<ul role=menu focusgroup="block wrap" autofocus id=aboutpop aria-label=About popover>
<li role=none><a role=menuitem href="…" tabindex=-1 title="">Overview</a></li>
<li role=none><a role=menuitem href="…" tabindex=-1 title="">Administration</a></li>
</ul>
</li>
<li role=none>
<a role=menuitem popovertarget=admitpop href="…" tabindex=-1 title="">Admissions</a>
<ul role=menu focusgroup="block wrap" autofocus id=admitpop aria-label=Admissions popover>
<li role=none><a role=menuitem href="…" tabindex=-1 title="">Apply</a></li>
<li role=none>
<a role=menuitem popovertarget=tuitpop href="…" tabindex=-1 title="">Tuition</a>
<ul role=menu focusgroup="block wrap" autofocus id=tuitpop aria-label=Tuition popover>
<li role=none><a role=menuitem href="…" tabindex=-1 title="">Undergraduate</a></li>
<li role=none><a role=menuitem href="…" tabindex=-1 title="">Graduate</a></li>
</ul>
</li>
<li role=none><a role=menuitem href="…" tabindex=-1 title="">Visit</a></li>
</ul>
</li>
</ul>
What to notice:
focusgroup
declarations can be nested inside of other focusgroups. When a nested focusgroup is declared on an element, it creates a new focusgroup and opts-out of its ancestor focusgroup.role=menubar
are limited to inline-direction arrow keys (e.g., left and right), while menuitems in role=menu
are limited to block-direction arrow keys (e.g., up and down). This allows the orthogonal arrow keys (e.g., up and down on the menubar, left and right on the menus) to be used for activation purposes (extra code that is not shown in the example).focusgroup
(nested focusgroups are completely independent of their ancestor focusgroup). In this case, the focus placement is handled by built-in popover
and autofocus
attribute behaviors.focusgroup
s is reset when the content is hidden/shown—this allows the autofocus
attribute to pick the first focusable element each time a menu is shown—the desired behavior in this case.Here, the author is adding up/down arrow key behavior to focusable headings using an accordion pattern. The arrow keys move the focus between the headings, but skip anything in the accordion bodies.
<div focusgroup=block>
<h3><button type=button aria-expanded=true aria-controls=sec1 id=h1>Heading 1</button></h3>
<div focusgroup=none role=region aria-labelledby=h1 id=sec1>
… accordion panel w/form controls that are Tab focusable …
</div>
<h3><button type=button aria-expanded=false aria-controls=sec2 id=h2>Heading 2</button></h3>
<div focusgroup=none role=region aria-labelledby=h2 id=sec2>
… 2nd panel …
</div>
<h3><button type=button aria-expanded=false aria-controls=sec3 id=h3>Heading 3</button></h3>
<div focusgroup=none role=region aria-labelledby=h3 id=sec3>
… 3rd panel …
</div>
</div>
What to notice:
focusgroup
is limited to the ‘block’ direction (assuming this content has a writing-mode
horizontal-tb
). The headers also participate in the sequential focus navigation (i.e., tab order)—focusgroup
does not change that.role=region
sections is excluded from arrow key navigation via the inclusive opt-out focusgroup=none
.role=region
is still available via sequential focus navigation (e.g., tab order). The tab order is unaffected by focusgroup=none
declarations.Finally, the author is creating a data grid following the data grid pattern where each of the cells in the table are focusable.
<table focusgroup=grid role=grid aria-label=Transactions>
<tbody><tr>
<th>Date</th>
<th>Type</th>
<th>Description</th>
<th>Amount</th>
<th>Balance</th>
</tr>
<tr>
<td tabindex=-1>01-Jan-16</td>
<td tabindex=-1>Deposit</td>
<td><a tabindex=-1 href=#>Cash Deposit</a></td>
<td tabindex=-1>$1,000,000.00</td>
<td tabindex=-1>$1,000,000.00</td>
</tr>
<tr>
<td tabindex=-1>02-Jan-16</td>
<td tabindex=-1>Debit</td>
<td><a tabindex=-1 href=#>Down Town Grocery</a></td>
<td tabindex=-1>$250.00</td>
<td tabindex=-1>$999,750.00</td>
</tr>
</tbody></table>
What to notice:
focusgroup=grid
understands table layout and will provide logical cell navigation with arrow keys around all the focusable grid cells.<th>
header cells are not made focusable in this example, and so are not navigable by the focusgroup
.tabindex=-1
to take them out of sequential focus navigation. The focusgroup
ensures that at least one of these focusable elements participates in the sequential focus navigation order regardless. The focusgroup
also remembers the last focused element, and returns the user to that element when they re-enter the table via sequential focus navigation.The goal of this feature is to “pave the cow path” of an existing authoring practice (and accessibility best practice) implemented in nearly every Web UI library: the roving tabindex [react, angular, fluent, elix]]. Note however, that certain design choices have been made to generalize the behavior so that additional scenarios are possible. See comparing roving tabindex and focusgroup for further details.
To achieve this goal, we believe the solution must be available in declarative markup or CSS. If JavaScript is required, then there seems little advantage to using a built-in feature over what can be implemented completely in author code. Furthermore, a declarative solution provides the key signal that allows the platform’s accessibility infrastructure to make the focusgroup
accessible by default:
In some control patterns (such as radio groups or tablists) moving the focus to an element also toggles its selection state. While some use cases will require the selection state to follow the focus, in others these need to be decoupled. focusgroup
is decoupled from selection. Tracking and changing selection based on focus will require author code. Note that a related proposal for tracking selection state, CSS Toggles, is no longer being pursued.
Implementations are welcome to experiment with additional UI (e.g., a “focusgroup focus ring”) in order to help make users aware of focusgroups, however this proposal does not include any specific guidelines or recommendations.
tabindex
).horizontal-tb
writing mode) or block-axis navigation keys or both (to trivially reserve one axis of arrow key behavior for supplementary actions, such as opening nodes in a tree view control). See CSS Logical Properties and Values for more about logical directions.<table>
-structured content or other grid-like structured content).A use case we are evaluating:
display: grid
to provide 2d grid navigation.A focusgroup is a group of related elements that can be navigated by directional arrow keys and home/end keys and for which the web platform provides the navigation behavior by default. No JavaScript event handlers needed in many cases! The arrow keys controlling the navigation are mapped to “forward” and “reverse” directions according to whether they point in the “block/inline-end” or “block/inline-start” directions, respectively (based on the element’s directionality that declares the focusgroup).
There are two kinds of focusgroups: linear focusgroups and grid focusgroups. Linear focusgroups provide arrow key navigation among a list of elements. Grid focusgroups provide arrow key navigation behavior for tabular (or 2-dimensional) data structures.
HTML | CSS | Explanation |
---|---|---|
focusgroup or focusgroup="" (no value specified) | focus-group-type: linear | Defines a linear focusgroup |
focusgroup=grid or focusgroup=manual-grid | focus-group-type: grid or focus-group-type: manual-grid | Defines an automatic grid or manual grid focusgroup. Grid focusgroup are covered in more detail below |
In the case that HTML attribute values conflict with CSS properties, the CSS values override the HTML-defined values.
Focusgroups consist of a focusgroup definition that establishes focusgroup candidates and focusgroup items. Focusgroup definitions manage the desired behavior for the associated focusgroup items. Focusgroup items are the elements that actually participate in the focusgroup (from the set of focusgroup candidates). Focusgroup candidates are all the elements under the scope of a focusgroup definition. The focusgroup scope consists of the element with the focusgroup definition and its shadow-inclusive descendants, excluding elements that have opted out.
The minimal focusgroup below demonstrates that the element declaring a focusgroup is also a focusgroup candidate and (in this case) the single focusgroup item.
<p tabindex=0 focusgroup>World's smallest, but least useful focusgroup</p>
Focusgroup candidates become focusgroup items if they are focusable, e.g., implicitly focusable elements like <button>
, or explicitly made focusable via tabindex
or some other mechanism (e.g., contenteditable
, being a scroll-container).
An element can only have one focusgroup definition added via the focusgroup
attribute or CSS focus-group-type: linear
property:
Example:
<p id="ancestor" focusgroup>
<span>
<span>
<span>
Some text and
<a id="one" href="…">a link</a>. A
<span id="two" tabindex="-1">focusable span</span> and
<a id="three" href="…">another link</a>.
</span>
</span>
</span>
</p>
The ancestor
element has the focusgroup definition. The elements with id=one
, two
, and three
(and any other shadow-inclusive descendants of ancestor
that may be added) are focusgroup candidates. Because each of these candidates are focusable, they are also focusgroup items. When one of the focusgroup items becomes focused, the user can move focus sequentially among all the focusgroup items using the arrow keys (up/right moves focus forward, down/left moves focus backwards assuming the <p>
element has writing-mode
horizontal-tb
and direction
ltr
).
Note that only elements with id=one
and three
can be focused using the Tab key. The <span>
has tabindex=-1
set, which takes it out of the tabindex sequential navigation ordering.
By default, focusgroups will remember the last-focused element, and for sequential focus navigation, will restore focus to that element when a focusgroup is re-entered. This is important for large lists or tables so that users are returned the the context they previously left without having to navigate from the start or end sequentially.
The focusgroup’s memory is initially empty. In that state, sequential focus navigation will pick the next element to focus using existing platform behavior with the exception noted below.
The focusgroup’s memory is cleared whenever there is any change to the focusability of its remembered element or the relationship that the element has with its focusgroup definition element (such as either one being removed from the tree). See additional details below.
Focusgroups provide a special behavior when used in conjunction with sequential focus navigation (“tab navigation”). Focusgroups ensure that at least one focusgroup item will participate in sequential focus navigation even if all focusgroup items are declared to opt-out via tabindex=-1
. This behavior ensures that a focusgroup can always be entered via sequential focus naviation. See below for further details. Aside from this one behavior change, there is no other impact to the way tab navigation works with tabindex
nor the tab ordering behavior.
Focusgroups can therefore be used to provide a roving tabindex among a set of related focusable controls such as this menubar:
Example:
<div role="toolbar" focusgroup aria-label="Text Formatting" aria-controls="…">
<div>
<button type="button" aria-pressed="false" value="bold" tabindex="-1"><span>Bold</span></button>
<button type="button" aria-pressed="false" value="italic" tabindex="-1"><span>Italic</span></button>
<button type="button" aria-pressed="false" value="underline" tabindex="-1"><span>Underline</span></button>
</div>
</div>
When pressing tab to enter this “toolbar” focusgroup from an element before it, focus will go to the first <button>
because:
tabindex
>= 0 or that is sequentially focusable by default (these <button>
s are taken out of sequential focus navigation with tabindex=-1
).At this point, the user can use the arrow keys to move from the beginning of the toolbar to the end, or press tab again to move outside of the focusgroup.
Alternatively, focusgroup can be used to supplement existing tab-stop behavior to provide arrow key navigational support in addition to tab navigation. No problem: just ensure the deisred elements are sequentially focusable via tabindex=0
(or stop excluding them via tabindex=-1
):
Example:
<div role="toolbar" focusgroup aria-label="Text Formatting" aria-controls="…">
<div>
<button type="button" aria-pressed="false" value="bold"><span>Bold</span></button>
<button type="button" aria-pressed="false" value="italic"><span>Italic</span></button>
<button type="button" aria-pressed="false" value="underline"><span>Underline</span></button>
</div>
</div>
Focusgroup definitions apply across Shadow DOM boundaries in order to make it easy for component developers to support focusgroup behavior across component boundaries. (Component authors that want to opt-out of this behavior can do so.)
Example:
<list-component focusgroup role="listbox" aria-label="Cute dogs">
<template shadowrootmode="open">
<my-listitem role="option" tabindex="0" aria-selected="true">Terrier</my-listitem>
<my-listitem role="option" tabindex="-1" aria-selected="false">Dalmation</my-listitem>
<my-listitem role="option" tabindex="-1" aria-selected="false">Saint Bernard</my-listitem>
</template>
</list-component>
The focusgroup is a default handler for certain keystrokes (keydown events for arrow keys, home/end, etc.) that will cause focus to move among focusgroup items. This default keyboard handling could interfere with other actions the application would like to take. A common pattern is to limit focusgroup directionality so that certain cross-axis keystrokes won’t trigger focusgroup behavior. However, if these doesn’t address the use case, then authors may cancel the focusgroup’s default behavior at any time by canceling (preventDefault()
) the specific keydown event. Keydown events are dispatched by the currently focused element, and bubble through the focusgroup ancestor element in most cases.
Some built-in controls like <input type=text>
provide keyboard behaviors that “trap” nearly all keys that would be handled by the focusgroup. Others such as <input type=number>
trap only certain keys like the arrow keys that are also used for focusgroup navigation. This proposal does not provide a built-in workaround to prevent this from happening. Instead, authors are advised to be sure users can “escape” these elements. Built-in elements provide this via the tab key. Other strategies might include requiring an “activation” step before putting focus into the interactive control (and an Esc key exit to leave).
The focusgroup’s memory may also cause unexpected user interactions if authors are not careful. For example, without any author mitigations, an interactive control inside a focusgroup may inadvertently prevent the user from accessing other focusgroup items:
<div role=toolbar focusgroup aria-label="Font Adjustment" aria-controls="…">
<label for=font-input>Font</label>
<div>
<div>
<input type=text id=font-input role=combobox aria-autocomplete=both aria-expanded=false aria-controls=font-listbox aria-activedescendant="">
<button type=button aria-label="Font List" aria-expanded=false aria-controls=font-listbox tabindex=-1>🔽</button>
</div>
<ul id=font-listbox role=listbox aria-label="Font List">
<li role=option>Ariel</li>
<li role=option>Monospace</li>
<li role=option>Verdana</li>
</ul>
</div>
<button type=button value="bigger" tabindex=-1><span>Increase Font</span></button>
<button type=button value="smaller" tabindex=-1><span>Decrease Font</span></button>
</div>
When the combobox
input element is focused, it is remembered by the focusgroup’s memory. The <input>
element traps nearly all keystrokes by default, including the arrow keys that might have been used to reach the “Increase/Decrease Font” buttons. When the user presses tab, focus exits the focusgroup. Later, when focus re-enters, the focusgroup will put focus back on the <input>
element (because of the memory), and the cycle continues with no way to get to the two following buttons via keyboard interaction alone.
Fortunately, there are several solutions to this problem:
tabindex=-1
from the “Increase/Decrease Font” buttons.combobox
. (Refer to “Avoid including controls whose operation requires the pair of arrow keys used for toolbar navigation” in the Toolbar control pattern.) Additionally, opt-out the <input>
control from focusgroup participation so that arrow keys skip it. Alternatively, turn off the focusgroup’s memory so that focus isn’t automatically returned to the combobox
.<input>
and move focus manually. Also consider limiting the focusgroup to one axis and reserving the other axis for operating the <input>
.Because focusgroup definitions are intended for grouping related controls, it does not make sense to provide focusgroup functionality on all elements. While the focusgroup attribute may de defined as a global attribute, it’s applicability is limited to a subset of elements.
At the time of writing, we are evaluating which elements will honor a focusgroup attribute definition. We welcome comments on this section. To submit your feedback, please see the issue Restrict usage of focusgroup on certain elements.
The current proposal is to limit focusgroup to only the elements whose name match the DOM’s valid shadow host names (which are the elements allowed to call attachShadow()
). However, <table>
and some table parts will need to be an exception in order to properly support grid focusgroups.
To enable feature detection, the DOM will include a focusgroup
property, whose existence on elements is useful for feature detection.
partial interface HTMLElement {
[CEReactions] attribute DOMString focusgroup;
};
Focusgroups have the following additional features:
no-wrap
, which means that focus is not moved past the ends of a focusgroup with the arrow keys. wrap
and other tabular wrapping behaviors are available for grid
focusgroups.In HTML these feature options are applied as space-separated token values to the focusgroup
attribute. In CSS, these definitions are specified as properties (including a focus-group
shorthand property for convenience).
By default, focusgroup traversal with arrow keys ends at boundaries of the focusgroup (the start and end of a linear focusgroup, and the start and end of both rows and columns in a grid focusgroup). The following focusgroup definition values change this behavior:
HTML | CSS | Explanation |
---|---|---|
(default) focusgroup="" (unspecified) | focus-group-wrap: none | Disables any kind of wrapping. |
focusgroup=wrap | focus-group-wrap: wrap | Causes movement beyond the ends of the focusgroup to wrap around to the other side. |
Additional values are available for grid focusgroups. Note, row and column-specific wrapping and flowing can be specified together: focus-group-wrap
supports two-token specifiers for this purpose.
HTML | CSS | Explanation |
---|---|---|
(default) focusgroup="grid" | focus-group-wrap: none or focus-group-wrap: row-none col-none | Rows and columns do not wrap or flow (default). |
focusgroup="grid wrap" | focus-group-wrap: wrap or focus-group-wrap: row-wrap col-wrap | Movement at the ends of the rows/columns wrap around to the opposite side of the same rows/columns. |
focusgroup="grid row-wrap" | focus-group-wrap: row-wrap or focus-group-wrap: row-wrap col-none | Rows wrap around, columns do not wrap. |
focusgroup="grid col-wrap" | focus-group-wrap: col-wrap or focus-group-wrap: col-wrap row-none | Columns wrap around, rows do not wrap. |
focusgroup="grid flow" | focus-group-wrap: flow or focus-group-wrap: row-flow col-flow | Movement past the end of a row wraps the focus to the beginning of the next row. Movement past the beginning of a row wraps focus back to the end of the prior row. Same for columns. The last row/column wraps to the first row/column and vice versa. |
focusgroup="grid row-flow" | focus-group-wrap: row-flow or focus-group-wrap: row-flow col-none | Rows “flow” from row ends to the next/prior row as described above, columns don’t wrap or flow. |
focusgroup="grid col-flow" | focus-group-wrap: col-flow or focus-group-wrap: col-flow row-none | Columns “flow” from column ends to the next/prior column as described above, rows don’t wrap or flow. |
focusgroup="grid row-wrap col-flow" | focus-group-wrap: row-wrap col-flow | Rows wrap around, and columns flow (as described above). |
focusgroup="grid row-flow col-wrap" | focus-group-wrap: row-flow col-wrap | Rows flow (as described below) and columns wrap around. |
Specifying both row-wrap
and row-flow
in one HTML focusgroup definition is an author error. Only one declaration for row behavior is allowed. Similarily for col-wrap
and col-flow
.
In many cases, having multi-axis directional movement (e.g., both right arrow and down arrow linked to the forward direction) is not desirable, such as when implementing a tablist control pattern, in which case it may not make sense for the up and down arrows to also move the focus left and right. Likewise, when moving up and down in a vertical menu, the author might wish to use JavaScript to provide other behavior for the left and right arrow keys such as opening or closing sub-menus. In these situations, authors can limit the linear focusgroup to one-axis traversal.
Note that the following only apply to linear focusgroup definitions (they have no effect on grid focusgroups).
HTML | CSS | Explanation |
---|---|---|
(default) focusgroup="" (unspecified) | focus-group-direction: both | The focusgroup items will respond to forward and backward movement with both directions (horizontal and vertical). The default/initial value. |
focusgroup=inline | focus-group-direction: inline | The focusgroup items will respond to forward and backward movement only with arrow keys that are parallel to this element’s “inline” axis (e.g., left and right arrow keys for horizontal-tb writing mode). |
focusgroup=block | focus-group-direction: block | The focusgroup items will respond to forward and backward movement only with arrow keys that are parallel to this element’s “block” axis (e.g., up and down arrow keys for vertical-* writing modes). |
Example:
<tab-group role=tablist focusgroup="inline wrap">
<a-tab role=tab tabindex=0 aria-selected=true aria-controls="…">…</a-tab>
<a-tab role=tab tabindex=-1 aria-selected=false aria-controls="…">…</a-tab>
<a-tab role=tab tabindex=-1 aria-selected=false aria-controls="…">…</a-tab>
</tab-group>
In the above example, when the focus is on the first <a-tab>
element, pressing either the up or down arrow key does nothing because the focusgroup is configured to only respond to the inline (left/right in this case) arrow keys.
Because 2-axis directionality is the default, specifying both inline
and block
at the same time on one focusgroup is not allowed:
Example:
<!-- This is an example of what NOT TO DO -->
<radiobutton-group focusgroup="inline block wrap" role=radiogroup>
⚠️This focusgroup configuration is an error--neither constraint will be applied (which is actually
what the author intended).
</radiobutton-group>
Focusgroup definitions assigned to an element create focusgroup candidates that include the element itself and all its shadow-inclusive descendant elements. Any element within that focusgroup scope that is (or becomes) focusable will automatically become a focusgroup item belonging to its ancestor’s focusgroup.
With such an expansive opt-in behavior, it is important to provide an opt-out for elements or element subtrees. For example: focusable elements that wish to remain in sequential focus navigation and have arrow key navigation pass them over; or, components nested across a Shadow DOM boundary that wish to be excluded from focusgroup participation.
Opting-out applies to the element making the declaration as well as its shadow-inclusive descendants.
To opt-out:
HTML | CSS | Explanation |
---|---|---|
focusgroup=none | focus-group-type: none | Opt-out of a focusgroup: this element and its shadow-inclusive descendants will not be considered focusgroup candidates. |
In the following example of an accordion pattern, the accordion’s panels opt-out of focusgroup
behavior so that any interactive content in a panel is bypassed when navigating among the accordion headers.
Example:
<div focusgroup=block>
<h3>
<button type=button aria-expanded=true aria-controls=sec1 id=h1>Heading 1</button>
</h3>
<div focusgroup=none role=region aria-labelledby=h1 id=sec1> … accordion panel w/form controls that are Tab focusable … </div>
<h3>
<button type=button aria-expanded=false aria-controls=sec2 id=h2>Heading 2</button>
</h3>
<div focusgroup=none role=region aria-labelledby=h2 id=sec2> .. 2nd panel … </div>
<h3>
<button type=button aria-expanded=false aria-controls=sec3 id=h3>Heading 3</button>
</h3>
<div focusgroup=none role=region aria-labelledby=h3 id=sec3> .. 3rd panel … </div>
</div>
When a focusgroup definition is appled to an element, it implicicly opts out of any ancestor’s focusgroups. This ensures that every element can only belong in one focusgroup at a time.
Example:
<div focusgroup> <!-- Parent -->
<div focusgroup> <!-- implicit focusgroup=none -->
<button>Am I a candidate, if yes, for which group?</button>
<div focusgroup></div> <!-- what happens here? -->
</div>
</div>
<-- Parent -->
is a focusgroup
that currently has no focusable candidates in it. If other children are added to it, then they and their descendants would be candidates for inclusion in that Parent focusgroup.<button>
is in the focusgroup declared by its parent.<-- what happens here? -->
declares another focusgroup with no focusable candidates. At the same time it is also opting itself out of inclusion in the focusgroup that includes the <button>
.By default, when a linear or grid focusgroup is defined it includes a “memory” of the last-focused element within its scope, initially empty. Each time the focus is changed within a focusgroup, the “memory” is updated to refer to that element. This behavior is akin to the roving tabindex in which the “memory” is the statefull tabindex=0
value assigned to the currently focused element.
In some scenarios it is not desireable to have a focusgroup maintain a memory. Usually this is because there is a more relevant element which should take focus when entering the focusgroup instead of the most-recently-focused element. For example, an active (selected) tab in a role=tablist
container, rather than the last-focused tab (when selection does not follow focus).
To disable the focusgroup’s default memory, use the value no-memory
:
HTML | CSS | Explanation |
---|---|---|
(default) focusgroup="" (unspecified) | focus-group-memory: auto | The focusgroup will remember the last-focused element and redirect focus to it when entered via sequential focus navigation. |
focusgroup=no-memory | focus-group-memory: none | The focusgroup will not remember the last-focused element. |
After the focusgroup’s memory has been set, it must be cleared whenever any one of the following change:
hidden
or un-hidden; or if the currently-remembered element is hidden
or un-hidden.disabled
or inert
status changed; or if the currently-remembered element has its disabled
or inert
status changed.<div>
with a tabindex
has its tabindex
attribute removed).focusgroup=none
on itself or a shadow-inclusive ancester, or by changing focusgroups: if a new focusgroup definition appears on itself or one of its shadow-inclusive ancestor elements).To ensure that a focusgroup always has at least one tab stop in the sequential focus navigation order, and to provide the appropriate “hook” for a focusgroup’s “memory” to redirect focus to the last-focused element in a focusgroup, a change to sequential focus navigation is needed.
This change is intended to ensure that focus is directed to one of the following focusgroup candidates whenever focus enters a focusgroup. The first matching condition is always taken:
tabindex=0
(or other built-in element with UA-defined keyboard focusability like <input>
, <button>
, etc.) that is also a focusgroup candidate for the current focusgroup, focus will be set on the first such element in DOM order (regardless of the direction of traversal, i.e., via tab
or Shift+tab
). Note: this provides authors with a predictable “entry” of their choosing within a focusgroup.Specifically, each focusgroup definition must maintain a:
tabindex
, or with tabindex=0
, or that is a built-in sequentially focusable element that has not opted-out via tabindex=-1
).Algorithmically, during “forward” sequential focus navigation, if the element being considered is:
For “reverse” sequential focus navigation, the algorithm is similar, but swap occurances of the “first focusable focusgroup candidate” for “last focusable focusgroup candidate”.
Because this algorithm applies only when interrogating the first (or last) focusable focusgroup candidate, then any descendants that preceed (or follow) the first (or last) focusable focusgroup candidate that themselves define a focusgroup are considered first. In other words, broad-reaching ancestral focusgroups won’t necessarily “steal” focus from descendant focusgroups during sequential focus navigation.
Some focusable data is structured not as a series of nested linear groups, but as a 2-dimensional grid such as in a spreadsheet app, where focus can move logically from cell-to-cell either horizontally or vertically. In these data structures, it makes sense to support the user’s logical usage of the arrow keys to move around the data.
Grid navigation is expected to happen within well-structured content with consistent rows and columns where DOM structure reflects this organization. In focusgroup grid navigation, only the cells in the grid are focusgroup candidates and only the focusable cells become focusgroup items. It is not currently possible to use grid focusgroups to support other focusable tabular parts such as focusable row elements (see comment in issue 1018 for a possible future addition for this use case).
The arrow navigation in the grid (and in the previous non-grid scenarios) should reflect the accessible structure of the document, not the presentation view made possible with CSS. For example, it is easy to create views that visually appear grid-like, but do not make sense to navigate like a grid if considering that the data model is fundamentally a list, which is how users of accessibility technology would perceive it. Wrapping a list of contact cards on screen in a grid-like presentation allows for more content density on screen for sighted users. In that scenario, arrow key navigation to move linearly (left-to-right following the line-breaking across each line) through the contents makes sense (especially if these are alphabetized), but orthogonal movement through the “grid” (up/down when cards are aligned or in a masonry layout) jumps the focus to seemingly arbitrary locations. Multi-directional arrow key navigation may seem appropriate for sighted users that have the visual context only, but are not appropriate for assistive technology. In the case of the list-presented-as-a-grid scenario, a linear focusgroup will make the most sense for sighted as well as users with accessibility needs.
When considering using a grid focusgroup, be sure that the data is structured like a grid and that the structure makes semantic sense in two dimensions (and not just for a particular layout or presentation).
Tabular data can be structured using column-major or row-major organization. Given that HTML tables and ARIA attributes for grids (role=grid
, role=row
, role=gridcell
) only exist for row-major grid types, this proposal does not define grid focusgroup organization for column-major data structures (and assumes row-major data structures throughout).
Grid focusgroups can be created “automatically” or manually. Automatic grids use the context of existing HTML semantics for tables as the structural components necessary to provide grid-based navigation. Any elements with computed table layout are suitable for an automatic grid (e.g., display: table-row
in place of using a <tr>
elements). Manual grid creation requires annotating specific elements with their focusgroup grid component names.
Note: We are evaluating the suitability for CSS display: grid
to create automatic grids.
The automatic grid approach will be preferable when the grid contents are uniform and consistent and when re-using semantic elements for grids (typical). The manual approach may be necessary when the grid structure is not uniform or structurally inconsistent (atypical), and involves identifying the parts of the grid on specific focusgroup candidates using CSS.
Elements with the grid
focusgroup definition on the root element of the structural grid become automatic grid focusgroups. The implementation must attempt to validate the structure of the grid to ensure it has appropriate row and cell structures. In the event that the implementation cannot automatically determine the grid structure, then the definition is ignored (i.e., there is no fallback to a linear grid).
HTML | CSS | Explanation |
---|---|---|
focusgroup=grid | focus-group-type: grid | Establishes the root of an automatic grid focusgroup. Shadow-inclusive descendants of the automatic grid are identified and assigned focus-group-type: grid-row and focus-group-type: grid-cell focusgroup candidate status automatically. |
Example:
<table aria-label="Tic tac toe grid" role=grid focusgroup=grid>
<tr>
<td tabindex=-1></td>
<td tabindex=-1></td>
<td tabindex=-1></td>
</tr>
<tr>
<td tabindex=-1></td>
<td tabindex=-1></td>
<td tabindex=-1></td>
</tr>
<tr>
<td tabindex=-1></td>
<td tabindex=-1></td>
<td tabindex=-1></td>
</tr>
</table>
The <table>
’s grid focusgroup definition automatically establishes each of its descendant <tr>
s as focusgroup rows (the parser-generated <tbody>
is accounted for) and <td>
s as focusgroup cells. Each focusgroup cell is a scope root for one focusgroup item per cell, and the cell and its shadow-inclusive decendants are all focusgroup candidates. Among all focusgroup cells, the left/right arrow keys navigate between cells in the table, and up/down arrow keys will compute the new target based on the DOM position of the current focusgroup candidate cell in relation to the focusgroup candidate row.
A manual grid is declared in a focusgroup definition with the name manual-grid
. With a manual grid, the rows and cells must be explicitly indicated using grid-row
and grid-cell
:
HTML | CSS | Explanation |
---|---|---|
focusgroup=manual-grid | focus-group-type: manual-grid | Establishes the root of a manual grid focusgroup. Shadow-inclusive descendants of the manual grid must be identified with focus-group-type: grid-row and focus-group-type: grid-cell explicitly. |
focusgruop=grid-row | focus-group-type: grid-row | Must be a shadow-inclusive descendant of a grid focusgroup root (i.e., the manual-grid -named focusgroup element). |
focusgroup=grid-cell | focus-group-type: grid-cell | Must be a shadow-inclusive descendant of a grid focusgroup root (i.e., the manual-grid -named focusgroup element). Must also be a shadow-inclusive descendant of a focus-group-type: grid-row focusgroup candidate. |
Cells cannot be descendants of other cells, and rows cannot be descendants of other rows.
Each focusgroup candidate will perform an ancestor search to locate its nearest grid structural component: cells will look for their nearest row, and rows will look for their nearest grid root.
In the following example, the <my-cell>
s are all meant to be on the same row of the grid, and the rows are designated by <my-row>
elements:
Example:
<my-root role=grid focusgroup=manual-grid>
<div role=none class=presentational_wrapper>…</div>
<my-row role=row focusgroup=grid-row>
<first-thing role=gridcell focusgroup=grid-cell>…</first-thing>
<cell-container role=none>
<my-cell role=gridcell focusgroup=grid-cell>…</my-cell>
<my-cell role=gridcell focusgroup=grid-cell>…</my-cell>
</cell-container>
<cell-container role=none>
<my-cell role=gridcell focusgroup=grid-cell>…</my-cell>
<my-cell role=gridcell focusgroup=grid-cell>…</my-cell>
</cell-container>
</my-row focusgroup=grid-row>
<!-- repeat pattern of div/my-row pairs... -->
</my-root>
The following non-uniform structure can still have grid semantics added via manual-grid
:
Example:
<div role=grid focusgroup="manual-grid flow">
<div role=row focusgroup=grid-row>
<div>
<div role=gridcell focusgroup=grid-cell></div>
<div role=gridcell focusgroup=grid-cell></div>
</div>
</div>
<div>
<div role="row" focusgroup=grid-row>
<div role=gridcell focusgroup=grid-cell></div>
<div role=gridcell focusgroup=grid-cell></div>
</div>
</div>
<div>
<div>
<div role="row" focusgroup=grid-row>
<div>
<div role=gridcell focusgroup=grid-cell></div>
<div role=gridcell focusgroup=grid-cell></div>
</div>
</div>
</div>
</div>
</div>
Unlike linear focusgroups, an automatic or manual grid focusgroup requires a small degree of DOM structure to work correctly. Unless the proper structure exists, the grid focusgroup won’t work.
Attempts to define new grid or linear focusgroups among the DOM elements that makeup the structure of a grid focusgroup (such as on or between elements defining the root grid container, the grid-rows and the grid-cells) will be ignored. However new grid or linear focusgroups can be defined on elements that are shadow-inclusive descendants of grid-cell elements (e.g., that are outside the set of elements making up the grid’s DOM structure).
Like linear focusgroups, focus is only set on elements that are focusable. The arrow key navigation algorithms look for the first focusgroup item (in DOM order) of a grid focusgroup cell in the direction the arrow was pressed. Non-focusable grid focusgroup candidates of a focusgroup cell are passed over in the search.
It is entirely possible to have rows with non-uniform numbers of cells. In these cases, focusgroup navigation behaviors may not work as visibly desired. Algorithms for navigating grid focusgroups will work based on content the grid content structure as specified. If the algorithms conclude that there is no “next candidate cell” to move to (e.g., in a grid with two rows, and the bottom row has three cells, and the top row only two, if the focus is on the 3rd cell, a request to move “up” to the prior row cannot be honored because there is no “3rd cell” in that row.
No considerable privacy concerns are expected, but we welcome community feedback.
No significant security concerns are expected.
Here is a short list to issue discussions that led to the current design of focusgroup.
focusgroup
works across Shadow DOM boundaries by defaultWe considered various alternative solutions before arriving at the current proposal:
Since at least 2002 the CSS WG has defined related CSS properties nav-up
, nav-right
, nav-down
, nav-left
in CSS Basic UI 4’s Keyboard Control. These properties are expected to provide focus navigation control similar to focusgroup
. They differ in some significant ways:
nav-*
are defined using CSS (vs HTML). Not that HTML-without-CSS is really a modern concern, but proposing focusgroup in HTML enables the user agent to setup focusgroups without the CSS dependency (or behavior change should CSS application be delayed due to network conditions).nav-up
, etc., require an explicit content selector (id) for ordering, which makes them relatively brittle. This design presents at least three challenges:nav-*
selectors are not updated in terms of changes to document content (at authoring time) the navigation expectations can get out of sync.nav-*
properties in order to keep top/left/bottom/right navigation logical per the current layout.nav-left
actually navigates up or right) while also opening up the possibility of unidirectional navigation (e.g., after navigating right, the user can’t go “back” to the left due to missing or erroneous selectors).nav-*
properties has the unique property of making that element focusable upon keyboard navigation to it. Additional clarity on how this special semantic applies would be needed for implementation. It is unclear (especially given the related Note in the spec) if this is a desirable behavior.In nearly 20 years, user agents have not implemented these properties. It is likely that focusgroup
will create an incongruency in the platform with these nav-*
CSS properties. Should the two exist simultaneously, the nav-*
properties might provide override semantics for directional movement, and take precedence over the focusgroup
attribute. However, we hope that such a conflict will not occur.
Another approach to focusable navigation has been defined in CSS Spatial Navigation. This specification enables user agents to deterministically navigate focusable elements in logical directions (top, left, bottom, and right) according to those element’s visual presentation. It assumes that a user agent will use some undefined trigger to enter a “spatial navigation mode” in which the specification-defined behavior will apply. This mechanism of navigating content can be an alternative (or addition to) traditional TAB key navigation (or equivalent) for a variety of devices (like TVs).
Spatial navigation occurs in the context of a “spatial navigation container” which defaults to any scroll container (e.g., the viewport). The specification provides an API to enable programmatic navigation, and offers several CSS properties: to enable additional spatial navigation containers, to control the behavior of focus and scroll actions by navigation keys, and to customize the algorithm used for finding the next focusable element in a given direction.
The specification does not presume to use spatial navigation in a limited (e.g., scoped and grouped) way as focusgroup
implies. It appears to assume that arrow key direction navigation will be a primary navigation technique, thus any and all focusable content should participate.
It may be possible for Spatial Navigation and focusgroup
to coexist simultaneously in the same content. In the case that spatial navigation is only enabled via a special mode, it would be likely that its navigation model would take precedence over focusgroup
, as it is meant to make all visual, focusable content navigable, and would provide a super-set of navigational movement for visual content. It is worth noting that accessibility tools would not likely enable spatial navigation mode.
It seems desirable to have a feature to easily enable spatial-navigation-like behavior for a subset of elements in a presentation. While this would not be ideal for an accessibility-view of the content, it is possible that particular spatial navigation metaphors could be omitted by accessibility tools when performing navigation. For example, when an AT interfaces with the user agent, the AT might limit navigation to “forward/backward” content navigation modes, while a user not working with an AT (or ATs designed for visual navigation) would enable the full spatial navigation model. An opt-in for spatial navigation would certainly be a requirement and could be an extension to focusgroup
(e.g., focusgroup=spatial
).
<select>
). Then again, if the key navigation behavior is explained by the presence of an external attribute on these elements, perhaps the internal behavior should defer to the external specified behavior (usage of the attribute would cancel the element’s preexisting built-in behavior in favor of the new generic behavior). Implementation experience and additional community feedback will be necessary to land a reasonable plan for this case.See other open focusgroup issues on Github.
In addition to arrow keys, the focusgroup should also enable other navigation keys such as pageup/down for paginated movement (TBD on how this could be calculated and in what increments), as well as the home/end keys to jump to the beginning and end of groups.
It might also be interesting to add support for typeahead scenarios (though what values to look for when building an index would need to be worked out, and may ultimately prove to be too complicated).
Focusgroup types:
Description | HTML syntax | CSS syntax |
---|---|---|
no focusgroup | (missing/invalid attribute) | focus-group-type: none |
linear focusgroup | (unspecified value; default value) | focus-group-type: linear |
automatic grid focusgroup | grid | focus-group-type: grid |
manual grid focusgroup root | manual-grid | focus-group-type: manual-grid |
manual grid focusgroup row | grid-row | focus-group-type: grid-row |
manual grid focusgroup cell | grid-cell | focus-group-type: grid-cell |
Focusgroup directions:
Description | HTML syntax | CSS syntax |
---|---|---|
both directions for a linear focusgroup | (unspecified value; default value) | focus-group-direction: both |
inline direction for a linear focusgroup | inline | focus-group-direction: inline |
block direction for a linear focusgroup | block | focus-group-direction: block |
Focusgroup wrapping:
Description | HTML syntax | CSS syntax |
---|---|---|
no wrap | (unspecified value; default value) | focus-group-wrap: none |
wrap | wrap | focus-group-wrap: wrap |
flow (grid focusgroups only) | flow | focus-group-wrap: flow |
column specific | col-wrap , col-flow , col-none | focus-group-wrap: col-wrap or col-flow or col-none |
row specific | row-wrap , row-flow , row-none | focus-group-wrap: row-wrap or row-flow or row-none |
Focusgroup memory:
Description | HTML syntax | CSS syntax |
---|---|---|
enable memory | (unspecified value; default value) | focus-group-memory: auto |
disable memory | no-memory | focus-group-memory: none |
Thanks to the members of the OpenUI Community Group for their many contributions. Special thanks to those who have reviewed, commented on, filed issues, and talked with us offline about focusgroup. Your insight and ideas and contributions have been indispensible.