Skip to main content

Structural Navigation v.next

Release status

Structural Navigation is planned for the next OpenKeyNav release. It is not included in the current npm or CDN package.

Structural Navigation moves real focus among the page's existing keyboard targets through semantic landmarks, sections, headings, forms, fieldsets, and lists. Contexts guide the route; they do not become generated focus stops. Native Tab and Shift+Tab continue to use the browser's sequential focus behavior.

Quick start

Initialize OpenKeyNav, enable it, and then enter Structural Navigation:

const openKeyNav = new OpenKeyNav();

openKeyNav.init({
debug: {
keyboardAccessible: false,
},
});

openKeyNav.enable();
openKeyNav.enterStructuralNavigation();

For keyboard entry, enable OpenKeyNav with Shift+o, then press r without Shift. Entry preserves the current focus. A polite status describes the active context, and a non-focusable outline identifies that context visually.

Keyboard controls

Structural shortcuts match the configured modifiers exactly. Letter commands below are lowercase, unmodified keys unless the table says otherwise.

ActionDefault commandProgrammatic command
Enter Structural NavigationrenterStructuralNavigation()
Keep moving through native sequential focus orderShift+Tab / TabNative browser behavior
Move to the previous target in the active routeUnboundstructuralNavigate('previousTarget')
Move to the next target in the active routeUnboundstructuralNavigate('nextTarget')
Move to the previous lateral structural contextShift+LeftstructuralNavigate('previousSiblingContext')
Move to the next lateral structural contextShift+RightstructuralNavigate('nextSiblingContext')
Broaden to the parent contextShift+UpstructuralNavigate('broadenContext')
Narrow on the current path or advance one heading levelShift+DownstructuralNavigate('narrowContext')
Cycle to the previous typed routeUnboundstructuralNavigate('previousPeerContext')
Cycle to the next typed routeUnboundstructuralNavigate('nextPeerContext')
Hide the visible status without leaving the modeShift+Escape
Exit Structural NavigationAlt+rexitStructuralNavigation()

Pressing plain r again also exits when the focused page control does not own that character command. Alt+r is the default explicit exit chord.

Mental model

Structural Navigation maintains three related pieces of state:

  1. Targets are existing elements discovered from the active focus scope. OpenKeyNav uses the pinned tabbable dependency and excludes its own generated interface.
  2. Structural contexts form one canonical hierarchy derived from page semantics and application configuration. Every target has one innermost structural context and also belongs to that context's ancestors.
  3. Typed routes are optional application-supplied ordered groups that can overlap the structural hierarchy. A target keeps one element identity even when it belongs to several routes.

The active context selects the sequence used by previous and next target commands. Changing context can leave focus on the same target; selecting another target always moves real DOM focus.

Previous and next targets

Previous and next target commands use the active structural context's flattened target sequence or the active typed route. They do not wrap. When no discovered target currently has focus, next enters at the first target and previous enters at the last target.

These commands are unbound by default so applications can choose shortcuts that fit their own interaction model.

Lateral structural contexts

Shift+Left and Shift+Right move among lateral structural contexts without wrapping. True structural siblings are eligible. Heading-backed contexts can also move laterally across different parents when they share the same canonical hierarchy depth, regardless of their authored h1h6 rank. Entry focuses the destination context's first existing target.

These commands are different from typed-route cycling. Typed routes use previousPeerContext and nextPeerContext, which are unbound by default.

Broaden and narrow

Broadening selects the immediate structural parent and retains focus. Narrowing first selects the child on the current target's path and also retains focus.

If no such child exists, narrowing can scan forward without wrapping for the first nonempty context one canonical level deeper. For heading-backed contexts from h1 through h5, that fallback looks for the next authored heading level and focuses its first target. h6 is the fallback boundary, but an actual child containing the current target remains available.

Typed routes

Typed routes represent credible overlapping application relationships such as a workflow group, table row, or table column. Explicit typed-route cycling wraps through the structural route and the typed routes that contain the current target. Changing routes retains focus.

OpenKeyNav does not infer arbitrary overlap from links, repeated text, aria-controls, visual position, or shared labels. Automatic table row and column routes are not currently inferred.

Automatically derived structure

OpenKeyNav conservatively derives contexts from operative semantics including:

  • the active document or supplied root;
  • native and ARIA landmarks and named regions;
  • section and article elements;
  • heading-defined sections;
  • form and search regions;
  • fieldset and legend;
  • ol, ul, and semantic list structures; and
  • supported composite-widget boundaries.

Generic containers, inaccessible hidden semantics, and roles suppressed with role="none" or role="presentation" do not create automatic contexts. Redundant empty or equivalent layers are normalized, while authored heading levels remain selectable.

Configuration

Use modesConfig.structuralNavigation to constrain the active root, filter targets, add application contexts, define typed routes, customize key ownership, or change presentation.

const openKeyNav = new OpenKeyNav();

openKeyNav.init({
debug: {
keyboardAccessible: false,
},
modesConfig: {
structuralNavigation: {
activeRoot: () =>
document.querySelector('[data-application-scope]') || document,

targetFilter: target =>
!target.matches('[data-skip-structural-navigation]'),

structuralContexts: [
{
id: 'application-actions',
name: 'Application actions',
type: 'workflow',
boundary: () =>
document.querySelector('[data-application-actions]'),
required: true,
},
],

typedContexts: [
{
id: 'pending-actions',
name: 'Pending actions',
type: 'workflow',
provenance: 'application configuration',
priority: 10,
targets: () => Array.from(
document.querySelectorAll('[data-pending-action]'),
),
},
],

contextIndicator: {
enabled: true,
color: '#0088cc',
width: 3,
offset: 4,
},

status: {
enabled: true,
visible: true,
announcements: true,
dismissCommand: {
key: 'Escape',
shiftKey: true,
},
},

ownsKey: (event, composedPath) => ({
arrows: composedPath.some(node =>
node instanceof Element && node.matches?.('[data-chart-editor]'),
),
}),
},
},
});

Core options

OptionDefaultPurpose
enabledtrueMakes the mode available after OpenKeyNav itself is enabled.
activeRootnullSupplies an Element, Document, open ShadowRoot, selector, or resolver for the active focus scope.
includeProgrammaticfalseIncludes focusable tabindex="-1" targets. This makes the computed sequence differ from native Tab order.
targetFilternullReceives (target, {root, openKeyNav}) and returns whether a discovered target remains available.
structuralContexts[]Adds or refines structural contexts in the canonical hierarchy.
typedContexts[]Adds ordered, overlapping application routes.
ownsKeynullDeclares additional page or widget ownership of arrows, Escape, character keys, or all keys.
displayCheck'full'Selects tabbable visibility checking. Use 'none' only for environments such as jsdom that do not implement layout.
escapeExitsfalseAllows bare Escape to exit when the focused control does not own Escape.
exitCommandnullOverrides the default Alt+r exit command.
overrideModifier'altKey'Lets a user deliberately invoke a configured structural arrow command from a widget that normally owns arrows.

Command defaults

commands: {
previousTarget: null,
nextTarget: null,
previousSiblingContext: {key: 'ArrowLeft', shiftKey: true},
nextSiblingContext: {key: 'ArrowRight', shiftKey: true},
broadenContext: {key: 'ArrowUp', shiftKey: true},
narrowContext: {key: 'ArrowDown', shiftKey: true},
previousPeerContext: null,
nextPeerContext: null,
}

Each shortcut object can specify key, altKey, ctrlKey, metaKey, and shiftKey. Unspecified modifiers are treated as false.

Application-supplied structural contexts

A structural context descriptor can provide:

  • id: stable required identity;
  • name and type: concise user-facing identification;
  • boundary or element: a selector, DOM boundary, or resolver;
  • targets or members: a selector, iterable, or resolver for membership;
  • parentId: an explicit structural parent when needed;
  • order: deterministic ordering when a DOM boundary cannot provide it;
  • required: retains a meaningful context that normalization might otherwise collapse;
  • headingLevel: optional authored heading metadata; and
  • isValid(details): includes the descriptor only while application state makes it valid.

Configured members are filtered to the live discovered target inventory. Structural contexts must remain nested or disjoint; partially overlapping groups belong in typedContexts instead.

Application-supplied typed routes

A typed route descriptor can provide:

  • id: stable required identity;
  • name and type;
  • provenance: a debugging description of where the relationship came from;
  • priority: ordering relative to other typed routes;
  • targets or resolveTargets: an ordered iterable or resolver; and
  • isValid(details): optional application-state validation.

Typed-route targets cannot introduce arbitrary focus destinations. OpenKeyNav keeps only connected elements already present in the current discovered target inventory. Routes are ordered by priority and then registration order.

Keyboard ownership

Native Tab, Shift+Tab, Enter, Space, and bare arrow behavior passes through. Text editors, native selects, range and number inputs, radio groups, and ARIA composite widgets retain their character, arrow, and Escape interaction where applicable.

If a configured structural arrow command conflicts with an arrow-owning widget, hold Alt to invoke the structural command deliberately. Applications can declare additional ownership with ownsKey or markup such as:

<div data-openkeynav-key-owner="arrows escape">
<!-- Application-owned interaction -->
</div>

Supported ownership tokens are all, arrows, escape, and character. Unconfigured modifier chords pass through.

While Structural Navigation is active, the legacy h, configured 16, and s focus commands are suppressed so they cannot add temporary focus stops. Entering Click Mode, Move Mode, or the shortcut menu exits Structural Navigation first.

Status and context indicator

The default polite status reports the active structural or typed route, the underlying structural level, the current target's name and position, and the number of applicable alternate routes. It updates in place as focus or context changes.

Shift+Escape hides only the visible status for the rest of the current mode. Its visually hidden polite updates remain available to assistive technology. Re-entering the mode restores the visible surface.

The context indicator is a generated, aria-hidden, pointer-transparent outline around the active context. It never receives focus or enters target discovery. Its default color falls back to OpenKeyNav's focus outline color and then #0088cc.

Programmatic API

const openKeyNav = new OpenKeyNav().init({
debug: {
keyboardAccessible: false,
},
});

openKeyNav.enable();
openKeyNav.enterStructuralNavigation();
openKeyNav.structuralNavigate('nextTarget');
openKeyNav.structuralNavigate('nextSiblingContext');
openKeyNav.structuralNavigate('nextPeerContext');
const state = openKeyNav.getStructuralNavigationState();
openKeyNav.invalidateStructuralNavigation();
openKeyNav.exitStructuralNavigation();

Accepted navigation commands are:

  • previousTarget
  • nextTarget
  • previousSiblingContext
  • nextSiblingContext
  • broadenContext
  • narrowContext
  • previousPeerContext
  • nextPeerContext

enterStructuralNavigation() requires OpenKeyNav itself to be enabled. exitStructuralNavigation() preserves the current page focus. Treat the object returned by getStructuralNavigationState() as inspection state rather than a stable replacement for the public methods.

Dynamic pages and cleanup

OpenKeyNav observes relevant DOM, open Shadow DOM, and slot changes. Mutations mark the derived model dirty; they do not move focus. The next accepted command or focus synchronization rebuilds the model lazily while preserving live target identities and stable context IDs where possible.

Call invalidateStructuralNavigation() when a filter or context resolver depends on application state that changed without a corresponding DOM mutation. Mode exit, global disable, and destroy() release structural observers, status, and context-indicator state.

Focus scopes and limitations

  • The default scope is the document. A topmost native modal dialog constrains navigation while open. Applications can provide a narrower activeRoot for custom modals or focus traps.
  • Open Shadow DOM is traversed and deep focus is synchronized. Closed Shadow DOM is not inspected.
  • An iframe is one atomic outer-page target; Structural Navigation does not inspect its document.
  • Popovers can contribute structure but are not automatically treated as focus traps.
  • Native Tab remains authoritative. The computed target inventory uses tabbable 6.5.0 and can differ with browser, platform, and user preferences such as macOS link-focus settings.
  • A screen reader can consume arrow keys in browse or virtual-cursor mode before page JavaScript receives them. Structural Navigation moves real DOM focus when its configured events reach the page; it does not replace screen-reader browse navigation.
  • Automatic table row and column routes are not inferred. Supply explicit typed routes when that relationship is credible.
  • Setting includeProgrammatic: true includes programmatic-only targets and no longer produces a native-Tab-equivalent sequence.
  • Current automated browser verification covers Chromium. Firefox, WebKit, keyboard-layout, and assistive-technology coverage require additional testing.

Research basis

The peer-routing design is inspired by Mei et al., Benthic: Perceptually Congruent Structures for Accessible Charts and Diagrams (ASSETS 2025). Benthic motivates this adaptation; it does not validate automatic structure inference for arbitrary webpages. OpenKeyNav therefore prefers a canonical structural hierarchy and accepts overlapping routes only from credible, testable application or platform semantics.