Structural Navigation
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.
| Action | Default command | Programmatic command |
|---|---|---|
| Enter Structural Navigation | r | enterStructuralNavigation() |
| Keep moving through native sequential focus order | Shift+Tab / Tab | Native browser behavior |
| Move to the previous target in the active route | Unbound | structuralNavigate('previousTarget') |
| Move to the next target in the active route | Unbound | structuralNavigate('nextTarget') |
| Move to the previous lateral structural context | Shift+Left | structuralNavigate('previousSiblingContext') |
| Move to the next lateral structural context | Shift+Right | structuralNavigate('nextSiblingContext') |
| Broaden to the parent context | Shift+Up | structuralNavigate('broadenContext') |
| Narrow on the current path or advance one heading level | Shift+Down | structuralNavigate('narrowContext') |
| Cycle to the previous typed route | Unbound | structuralNavigate('previousPeerContext') |
| Cycle to the next typed route | Unbound | structuralNavigate('nextPeerContext') |
| Hide the visible status without leaving the mode | Shift+Escape | — |
| Exit Structural Navigation | Alt+r | exitStructuralNavigation() |
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:
- Targets are existing elements discovered from the active focus scope. OpenKeyNav uses the pinned
tabbabledependency and excludes its own generated interface. - 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.
- 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.
Navigation behavior
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 h1–h6 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;
sectionandarticleelements;- heading-defined sections;
formandsearchregions;fieldsetandlegend;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
| Option | Default | Purpose |
|---|---|---|
enabled | true | Makes the mode available after OpenKeyNav itself is enabled. |
activeRoot | null | Supplies an Element, Document, open ShadowRoot, selector, or resolver for the active focus scope. |
includeProgrammatic | false | Includes focusable tabindex="-1" targets. This makes the computed sequence differ from native Tab order. |
targetFilter | null | Receives (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. |
ownsKey | null | Declares 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. |
escapeExits | false | Allows bare Escape to exit when the focused control does not own Escape. |
exitCommand | null | Overrides 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;nameandtype: concise user-facing identification;boundaryorelement: a selector, DOM boundary, or resolver;targetsormembers: 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; andisValid(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;nameandtype;provenance: a debugging description of where the relationship came from;priority: ordering relative to other typed routes;targetsorresolveTargets: an ordered iterable or resolver; andisValid(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 1–6, 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:
previousTargetnextTargetpreviousSiblingContextnextSiblingContextbroadenContextnarrowContextpreviousPeerContextnextPeerContext
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
dialogconstrains navigation while open. Applications can provide a narroweractiveRootfor 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
Tabremains authoritative. The computed target inventory usestabbable6.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: trueincludes 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.