Skip to main content

Structural Navigation

Structural Navigation moves real focus among the page's existing keyboard targets through semantic landmarks, sections, headings, forms, fieldsets, and lists. Structural contexts organize those targets into routes, while native Tab and Shift+Tab continue to follow the browser's sequential focus order.

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 to enter Structural Navigation. Entry preserves the current focus. A polite live status announces the active context and remains visually hidden by default, while compact keylabels show the available routes and identify the actively focused target. A dashed context outline appears transiently during structural context movement; enable the persistent indicator when the host interface benefits from a continuous enclosing-context cue.

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 structural context startUnboundstructuralNavigate('previousContextStart')
Move to the next structural context startUnboundstructuralNavigate('nextContextStart')
Move to the previous target in the active routeUnboundstructuralNavigate('previousTarget')
Move to the next target in the active routeUnboundstructuralNavigate('nextTarget')
Move to the previous semantic regionShift+LeftstructuralNavigate('previousSiblingContext')
Move to the next semantic regionShift+RightstructuralNavigate('nextSiblingContext')
Move up to a shallower authored heading levelShift+UpstructuralNavigate('broadenContext')
Move down to a deeper authored heading levelShift+DownstructuralNavigate('narrowContext')
Cycle to the previous typed routeUnboundstructuralNavigate('previousPeerContext')
Cycle to the next typed routeUnboundstructuralNavigate('nextPeerContext')
Hide the visible debug status without leaving the modeShift+Escape
Exit Structural NavigationAlt+rexitStructuralNavigation()

Pressing plain r again also exits whenever that character command is available to OpenKeyNav. Alt+r is the default explicit exit chord.

Mental model

Structural Navigation maintains three related pieces of state:

  1. Targets are existing page elements discovered within the active focus scope.
  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. Movement stops at each end of the route. When focus starts outside the discovered targets, next enters at the first target and previous enters at the last target.

These commands begin with empty key bindings, giving applications control of shortcuts that fit their interaction model.

Previous and next semantic regions

Shift+Left and Shift+Right move through direct semantic contexts in native focus order and stop at the route boundaries. Consecutive targets in one context form a single route stop, and movement focuses the destination context's first existing keyboard target.

The route includes headed and unheaded landmarks, named regions, sections, articles, forms and search regions, fieldsets, semantic lists and list items, ARIA composite-widget boundaries, and configured application regions. Page or root content also forms a stop when no narrower semantic context applies. Heading rank neither filters nor reorders this region route.

Use h to move through all headings whose contexts contain keyboard targets, or 1 through 6 to move through one authored heading level. These heading commands focus each heading context's first keyboard target rather than moving focus to the heading itself. Repeating a command continues from the current focus.

Typed-route cycling uses the separate previousPeerContext and nextPeerContext commands, which begin with empty key bindings for application configuration.

Broaden and narrow

Shift+Up follows the authored heading outline to the nearest preceding heading with a lower rank number and focuses that context's first existing target. Shift+Down moves to the closest available deeper authored heading level and focuses its first existing target. The downward route includes skipped levels: an h2 can move directly to an eligible h4 when no eligible h3 context is available.

From a route without an authored heading, Shift+Down enters the first context at the shallowest authored level present, while Shift+Up enters the last context at the deepest authored level present. Neither command derives a heading level from a landmark, region, list, or DOM nesting.

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.

Applications define table rows, table columns, and other overlapping relationships as explicit typed routes. Use typedContexts to supply ordered target groups backed by application or platform semantics.

Automatically derived structure

OpenKeyNav 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
  • ARIA combobox, grid, listbox, menu, menubar, radiogroup, tablist, toolbar, tree, and treegrid boundaries.

Automatic contexts come from operative semantics. OpenKeyNav normalizes redundant empty or equivalent layers while keeping authored heading levels selectable. Use structuralContexts to add meaningful application regions whose structure is expressed outside these semantics.

When a native Tab stop wraps a heading, such as <a href="…"><h2>…</h2></a>, the wrapper remains the focus target and the first exposed heading it contains supplies that target's heading context. When one tabbable wrapper contains several headings, the first exposed heading wins.

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: {
debug: false,

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: false,
},

keylabels: {
enabled: true,
tab: true,
contextJump: true,
horizontal: true,
vertical: true,
nativeArrows: true,
activation: true,
},

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.
debugfalseShows the persistent structural status box when true; polite live announcements remain available either way.
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' for layout-free environments such as jsdom.
escapeExitsfalseAllows bare Escape to exit whenever Escape is available to the mode.
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.
statusPolite announcements enabled; debug box hiddenConfigures announcements and the visible debug status, including its Shift+Escape dismissal command.
keylabelsAll groups enabledConfigures Tab, configured context-start, structural-arrow, native-radio-arrow, and activation hints. Set an individual group or enabled to false to hide it.
contextIndicator{enabled: false}Keeps the dashed active-context outline visible throughout the mode when enabled. Heading-context commands still show it transiently by default.

Command defaults

commands: {
previousTarget: null,
nextTarget: null,
previousContextStart: null,
nextContextStart: 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.

Context-start commands use the same semantic-region route as Shift+Left and Shift+Right, but begin unbound. Applications can call them programmatically or assign independent shortcut objects. The overrideModifier option applies to structural arrow commands inside arrow-owning controls; it does not create context-start bindings. Native Tab and Shift+Tab remain untouched.

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. Use nested or disjoint groups for structural contexts and typedContexts for partially overlapping groups.

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 routes draw their targets from connected elements in the current discovered target inventory. OpenKeyNav preserves those live element identities and orders routes by priority and then registration order.

Keylabels show available routes

While Structural Navigation is the foreground mode, keylabels describe the focus routes available from the current target:

  • ⇧⇥ and identify the previous and next native Tab destinations only when that move crosses the direct semantic-context boundary used by horizontal region navigation; familiar in-context Tab movement remains unlabeled;
  • ⇧← and ⇧→ identify available previous and next semantic regions;
  • ⇧↑ and ⇧↓ identify available shallower and deeper heading-level destinations when those commands focus a different target;
  • explicitly configured context-start commands identify their destinations with their configured chords;
  • identifies Enter as the preferred activation key for a native control or focusable ARIA widget, while appears when Space is the sole standard activation key; and
  • native radio groups use ←↑ for the previous radio and →↓ for the next, or ↔↕ for the single peer in a two-radio group.

Tab, Shift+Tab, Enter, Space, and native radio-arrow behavior remain browser-owned. A target receives at most one label. Ownership-override arrows and configured context-start chords can use three symbols; other labels remain limited to two. When two symbols mean “or,” a divider separates them; chord symbols such as ⇧← remain joined. A labeled native Tab boundary route takes priority when it reaches the same destination as a structural route. An explicitly configured context-start route then takes priority over converging arrow routes.

The actively focused target's keylabel uses the configured focus color, darkened only as needed for at least 4.5:1 contrast with white text. Its thin white border and pointer remain consistent with other dark keylabels. Destination labels retain the normal spot palette. While a displayed modifier is held, its symbol highlights as a pressed key in every relevant label and returns to its normal treatment on release. This applies to both and the configured ownership override, such as . Set keylabels.tab, contextJump, horizontal, vertical, nativeArrows, or activation to false to hide one group, or set keylabels.enabled to false to hide all structural keylabels.

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, its destination label prepends ; 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.

Structural Navigation remains active until the user exits it. Its configured commands get first refusal; heading (h), heading-level (16), and scroll-region (s) navigation run normally without ending the mode. Click Mode, Move Mode, and the shortcut menu temporarily take keyboard priority, then Structural Navigation resumes when that temporary mode finishes or is dismissed. Page-owned character keys and unhandled Alt/Ctrl/Meta shortcuts continue to pass through. If two OpenKeyNav commands use the same chord, the configured Structural Navigation command has precedence when no temporary foreground mode is active.

Status identifies the route and authored heading level

The polite live status reports the active structural or typed route, the current target's name and position, and the number of applicable alternate routes. Heading-backed contexts report their authored heading level. Routes without an authored heading do not receive an invented numeric level. The status updates as focus or context changes and remains visually hidden by default.

Set modesConfig.structuralNavigation.debug to true to show the persistent debug box. While that box is enabled, Shift+Escape hides it for the rest of the current mode while polite live updates remain available to assistive technology. Re-entering the mode restores the debug surface.

The actively focused target retains the page's authored focus style, and OpenKeyNav's focused keylabel provides a persistent route anchor. A dashed neutral context outline appears while Shift+Up, Shift+Down, Shift+Left, or Shift+Right changes the active context, follows consecutive context moves, and clears on the next different action such as Tab. Set contextIndicator.enabled to true to keep it visible throughout the mode.

When the active context has an authored heading, a compact non-interactive tab attached to the outline shows h1 through h6. It reuses OpenKeyNav's existing neutral label palette, inherits the page typography, and does not create another style configuration. Unheaded contexts receive neither an invented level nor a level tab. The generated indicator is aria-hidden and pointer-transparent; page focus and target discovery remain on application content.

Programmatic API

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

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

Accepted navigation commands are:

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

enterStructuralNavigation() requires OpenKeyNav itself to be enabled. exitStructuralNavigation() preserves the current page focus. Use the object returned by getStructuralNavigationState() for inspection and the public methods to control the mode.

Dynamic pages and cleanup

OpenKeyNav observes relevant DOM, open Shadow DOM, and slot changes. Mutations mark the derived model dirty while focus stays in place. 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 integration paths

  • 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.
  • OpenKeyNav traverses open Shadow DOM and synchronizes deep focus. For controls inside closed roots, provide keyboard routes through the owning component.
  • Each iframe appears as one atomic outer-page target. Implement keyboard navigation within the framed document.
  • Popovers contribute their exposed structure. Supply an activeRoot for custom focus traps and modal scopes.
  • 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.
  • Structural Navigation moves real DOM focus when its configured events reach the page. Test its commands alongside screen-reader browse and virtual-cursor modes, which may consume arrow keys before page JavaScript receives them.
  • Supply explicit typed routes for table rows, table columns, and other overlapping application relationships.
  • Setting includeProgrammatic: true adds programmatic targets to the computed sequence beyond the native Tab order.
  • Current automated browser verification covers Chromium. Add Firefox, WebKit, keyboard-layout, and assistive-technology testing for the organization's supported environments.

Research basis

The peer-routing design takes inspiration from Mei et al., Benthic: Perceptually Congruent Structures for Accessible Charts and Diagrams (ASSETS 2025). OpenKeyNav applies that idea to webpages through a canonical hierarchy built from page semantics and overlapping routes supplied by testable application or platform relationships.