A GeckoView web extension providing WICG-compatible spatial navigation for Android TV, AAOS, and D-pad/keyboard navigation. Designed for seamless integration with flutter-geckoview and other GeckoView host applications.
- ✅ Geometric spatial navigation - Multi-pass scoring algorithm for accurate directional navigation
- ✅ Focus groups - Define navigation regions with
data-focus-groupattributes - ✅ WICG API compatibility -
window.navigate(),Element.spatialNavigationSearch(), etc. - ✅ Visual focus overlay - Animated amber outline with directional previews
- ✅ Scroll container awareness - Handles nested scrollable regions
- ✅ Native messaging - Bidirectional communication with GeckoView host apps
- ✅ Focus exit events - Notify native app when navigation leaves web content
- ✅ Config updates - Runtime configuration from native layer
- ✅ Shadow DOM traversal - Works with Web Components (Shoelace, Material Web)
- ✅ Virtual scroll detection - Automatic refresh for React Virtualized, YouTube, Twitter
- ✅ Focus trap detection - Handles modals/dialogs with escape affordances
- ✅ ARIA accessibility - Optional live region announcements
- ✅ Framework-aware refresh - Deferred updates for React/Vue/Angular
- ✅ Strict TypeScript - Fully typed codebase with
noImplicitAny: true - ✅ Performance - <5ms navigation latency (benchmarked for 1000+ elements)
# Configure npm to use GitHub Packages for @dart-technologies scope
echo "@dart-technologies:registry=https://npm.pkg.github.com" >> .npmrc
# Install the package
npm install @dart-technologies/spatial-navigation-geckoviewFor Flutter/native projects that bundle the extension as an asset:
# Add as submodule
git submodule add https://github.com/dart-technologies/spatial-navigation-geckoview.git lib/assets/spatial_navigation
# Initialize and update
git submodule update --init --recursive
# Build the extension
cd lib/assets/spatial_navigation
npm install
npm run build:allUpdate submodule to latest:
cd lib/assets/spatial_navigation
git pull origin main
npm install
npm run build:all
cd ../../..
git add lib/assets/spatial_navigation
git commit -m "chore: update spatial-navigation-geckoview submodule"Clone and build:
git clone https://github.com/dart-technologies/spatial-navigation-geckoview.git
cd spatial-navigation-geckoview
npm install
npm run build:all// Install extension in GeckoView
runtime.webExtensionController
.ensureBuiltIn(
"resource://android/assets/spatial_navigation/",
"spatial-navigation@geckoview.dev"
)
.accept(
{ extension ->
Log.i("SpatialNav", "Extension installed: ${extension.id}")
setupMessageDelegate(extension)
},
{ error -> Log.e("SpatialNav", "Install failed", error) }
)
// Handle messages from extension
private fun setupMessageDelegate(extension: WebExtension) {
extension.setMessageDelegate(object : WebExtension.MessageDelegate {
override fun onMessage(
nativeApp: String,
message: Any,
sender: WebExtension.MessageSender
): GeckoResult<Any>? {
val json = message as? JSONObject ?: return null
when (json.getString("type")) {
"spatialNavInit" -> Log.d("SpatialNav", "Initialized: ${json.getString("url")}")
"focusExit" -> handleFocusExit(json.getString("direction"))
}
return null
}
}, "geckoview-spatial-nav")
}<!-- Optional: Configure via global -->
<script>
window.spatialNavConfig = {
color: '#00BCD4', // Teal focus color
outlineWidth: 4,
autoRefocus: true,
enableAria: true, // Enable accessibility
traverseShadowDom: true, // For Web Components
};
</script>
<!-- Focus groups for navigation regions -->
<nav data-focus-group="main-nav;boundary=contain">
<button>Home</button>
<button>Search</button>
</nav>
<main data-focus-group="content;enterMode=last">
<!-- Content area remembers last focused element -->
</main>// WICG-compatible API
window.navigate('down'); // Move focus down
window.navigate('right'); // Move focus right
// Find next target without moving
const next = document.activeElement.spatialNavigationSearch('down');
console.log('Next target:', next);
// Get focusable elements in container
const focusables = document.body.focusableAreas({ mode: 'visible' });
// Get navigation container
const container = button.getSpatialNavigationContainer();All options can be set via window.spatialNavConfig:
| Option | Type | Default | Description |
|---|---|---|---|
color |
string | '#FFC107' |
Focus highlight color (amber) |
outlineWidth |
number | 3 |
Outline width in CSS pixels |
outlineOffset |
number | 3 |
Outline offset in CSS pixels |
overlayZIndex |
number | 2147483646 |
Overlay z-index |
arrowScale |
number | 1.0 |
Directional chevron scale |
disabledColor |
string | '128, 128, 128' |
Disabled/boundary indicator RGB string |
overlayTheme |
string | 'default' |
'default' or 'high-contrast' preset |
safeAreaMargin |
number | 12 |
Safe-area/overscan margin in CSS pixels |
overlayScrimOpacity |
number | 0.06 |
Inner scrim opacity (0–1) |
overlayGlowOpacity |
number | 0.35 |
Outer glow opacity (0–1) |
overlayGlowBlur |
number | 14 |
Outer glow blur radius in CSS pixels |
autoRefocus |
boolean | true |
Recover focus when lost |
| Option | Type | Default | Description |
|---|---|---|---|
observeMutations |
boolean | true |
Watch for DOM changes |
observeScroll |
boolean | true |
Update on scroll |
traverseShadowDom |
boolean | false |
Recurse into Shadow DOM |
observeVirtualContainers |
boolean | true |
Detect virtual scroll |
enableAria |
boolean | false |
Enable ARIA announcements |
focusTrapDetection |
boolean | false |
Detect modals/dialogs |
precomputeCandidates |
boolean | true |
Background pre-computation |
| Option | Type | Default | Description |
|---|---|---|---|
scoringMode |
string | 'geometric' |
Algorithm: 'geometric' or 'grid' |
distanceFunction |
string | 'euclidean' |
Distance: 'euclidean', 'manhattan', 'projected' |
overlapThreshold |
number | 0 |
Pixels of overlap allowed (BBC LRUD) |
gridAlignmentTolerance |
number | 20 |
Pixels tolerance for grid alignment |
wrapNavigation |
boolean | false |
Wrap focus at container boundaries |
useCSSProperties |
boolean | true |
Read --spatial-navigation-* CSS |
<div data-focus-group="id;boundary=contain;enterMode=last;remember=true">boundary:exit(default),contain,wrap,stopenterMode:default,first,lastremember:true(default),false
// Before focus changes (cancelable)
document.addEventListener('navbeforefocus', (e) => {
console.log('Moving', e.detail.dir, 'to', e.target);
// e.preventDefault() to cancel
});
// When hitting boundary
document.addEventListener('navnotarget', (e) => {
console.log('Boundary reached:', e.detail.dir);
if (e.detail.inTrap) {
console.log('In trap, escape:', e.detail.escapeKey);
}
});// When navigation leaves web content
document.addEventListener('spatialNavigationExit', (e) => {
console.log('Exiting web content:', e.detail.direction);
// Native app handles from here
});| Type | Payload | Description |
|---|---|---|
spatialNavInit |
{ url, version } |
Extension initialized |
focusChange |
{ direction, fromElement, toElement } |
Focus moved |
focusExit |
{ direction, inTrap } |
Reached boundary |
| Type | Payload | Description |
|---|---|---|
navigate |
{ direction } |
Request navigation |
configUpdate |
{ ...config } |
Update config |
refresh |
{} |
Re-scan focusables |
# Install dependencies
npm install
# Build all outputs
npm run build:all
# Build minified only
npm run build
# Build debug (unminified)
npm run build:debug
# Run tests
# Run tests
npm test
# Run performance benchmarks
npm run test:benchmark| File | Format | Size | Use Case |
|---|---|---|---|
dist/spatial-navigation.js |
UMD | ~20KB | General usage |
dist/spatial-navigation.esm.js |
ESM | ~20KB | Modern bundlers |
dist/spatial-navigation.extension.js |
IIFE | ~20KB | GeckoView extension |
dist/spatial-navigation.debug.js |
IIFE | ~50KB | Development |
| Feature | WICG Polyfill | This Extension |
|---|---|---|
| W3C API | Full | Partial |
| CSS Properties | Yes | Yes |
| Visual Overlay | No | Yes |
| Native Messaging | No | Yes |
| Virtual Scroll | No | Yes |
| Feature | Pathduck | This Extension |
|---|---|---|
| Sections | Yes | Focus Groups |
| Visual Feedback | Class only | Animated overlay |
| React/Vue | No | Framework-aware |
| Shadow DOM | No | Yes |
See CONTRIBUTING.md for development guidelines.
MIT