Project
←Back to projects
march 2, 2026
TabiNeko
A production-grade mouse gesture Chrome extension that lets you navigate tabs and pages with right-click drag gestures — built with TypeScript, Bun, and a custom Manifest V3 build pipeline.
TabiNeko — Mouse Gesture Navigation
TabiNeko is a Manifest V3 Chrome extension that replaces repetitive toolbar clicks and keyboard shortcuts with fluid right-click drag gestures. Navigate backward and forward, jump between tabs, scroll to page extremes, and refresh — without lifting a hand to reach the keyboard or precisely targeting a 24px button. It is built on a custom TypeScript + Bun build pipeline that works around Manifest V3's Content Security Policy constraints, and ships as a Chrome Web Store-ready ZIP artifact from a single bun run release command.
Overview
Opera shipped mouse gesture support in 2001. Firefox has had it via extensions since the early 2000s. Chrome has never shipped a native gesture system. Power users navigating dozens of pages per session accumulate hundreds of micro-movements — back button, next tab, scroll to bottom — that interrupt focus without being individually significant. TabiNeko treats gesture recognition as a first-class input method rather than an afterthought, with a mathematical classifier that handles imperfect hand-drawn paths reliably and a visual trail that gives users immediate feedback as they draw.
Architecture
TabiNeko separates into three independent runtime contexts, each compiled to its own output bundle, plus a static popup UI pre-rendered at build time.
Content Script — Gesture Tracking and Action Dispatch
The content script (src/scripts/content.ts) is injected into every HTTP and HTTPS page at document_idle. It owns three responsibilities: listening for mouse events, rendering the gesture trail, and dispatching the resolved action.
On mousedown with the right button, the script appends a full-viewport <canvas> element to the page and begins recording mouse position. The canvas scales for devicePixelRatio, so the trail renders crisp on retina displays. Path segments are drawn as quadratic Bézier curves rather than straight lines, which produces smooth visual trails even when the mouse moves in short, jerky steps.
On mouseup, the gesture signature returned by the classifier is resolved to a browser action. Page-level actions — history.back(), history.forward(), window.location.reload(), window.scrollTo() — execute directly in the page context. Tab operations send typed messages to the background service worker, which has the tabs permission required to query and activate tabs across the window.
Context menu suppression is handled by a one-shot contextmenu listener attached only when a gesture completes. If no gesture was recognized, the context menu fires normally — right-clicking without dragging works as expected.
Gesture Engine — Mathematical Classification
The gesture classifier (src/scripts/internal/gesture.ts) is a pure, stateless function: it takes a sequence of mouse coordinates and returns a string signature ("R", "L,U", "CIRCLE", etc.) or null. It has no side effects and no dependency on the DOM.
Direction detection uses a 25-pixel movement threshold from the last registered anchor point. A new cardinal direction (R, L, U, D) is only recorded when the cursor has moved more than 25px from the anchor in a dominant axis. This threshold filters cursor noise and minor wobble without requiring the user to draw with robotic precision. Consecutive identical directions are collapsed before the signature is finalized — ["R", "R", "D"] becomes ["R", "D"] — preventing jittery movement from generating phantom multi-directional gestures.
Circle detection uses a different approach: the classifier accumulates the signed angular change between consecutive mouse displacement vectors, computed via atan2 and the 2D cross-product. A closed circle is confirmed when two conditions hold simultaneously: accumulated rotation exceeds 300 degrees (tolerating imperfect closure), and the distance between the path's start and end points is less than 80 pixels. This geometric approach handles real hand-drawn circles reliably — users do not need to close the path perfectly for the gesture to register.
Background Service Worker — Tab Management
The background script (src/scripts/background.ts) is a minimal MV3 service worker handling exactly two message types: NEXT_TAB and PREV_TAB. On receipt, it queries all tabs in the current window, identifies the active tab's index, and activates the neighboring tab. Wrap-around is implemented — the last tab advances to the first, and the first tab retreats to the last. Tabs closed between gesture start and message delivery are caught and fail silently.
The service worker is intentionally minimal. Under Manifest V3, service workers are terminated when idle and restarted on demand, so no persistent state is stored in the background context.
Popup UI — Static Astro Output
The extension action popup (src/components/Popup.astro) is a static HTML page rendered by Astro at build time. It displays the gesture reference table and branding. No JavaScript executes in the popup at runtime — Astro's zero-JS-by-default output model produces a CSP-compliant page without additional post-processing.
Build Pipeline
Manifest V3's Content Security Policy prohibits inline scripts in extension pages. Astro's standard build output injects inline <script> tags, which would be blocked by the extension CSP. The build pipeline, composed entirely of Bun scripts, resolves this in four stages:
- Astro compiles
Popup.astroto static HTML and CSS indist/. bundler.tsruns Bun's native bundler oncontent.tsandbackground.ts, producing minifiedcontent.jsandbackground.jsindist/. Bun's built-in TypeScript support eliminates a separate transpilation step.extract-inline.tsparses the Astro-generated HTML, extracts any residual inline<script>blocks into separate.jsfiles, rewrites the<script src="...">attributes to reference them, and writes the modified HTML back. The output is unconditionally CSP-compliant.pack.tsZIPs thedist/folder into a versioned archive usingadm-zip— no OS-level ZIP tooling required, which makes the build reproducible on any platform.
The full pipeline runs in under two seconds on commodity hardware via bun run release, producing a Chrome Web Store-ready artifact.
Key Features
- Seven gesture vocabulary: Right, Left, Up, Down, Right+Down, Left+Up, and Circle map to Forward, Back, Scroll Top, Scroll Bottom, Next Tab, Previous Tab, and Refresh respectively. The signature space is intentionally small to keep the gestures distinct and muscle-memory-friendly.
- Mathematical circle detection: Accumulated signed angular change via
atan2and cross-product, with 300° rotation threshold and 80px closure tolerance. Handles imperfect hand-drawn circles that no directional classifier would catch. - 25px directional threshold: Filters cursor noise without requiring deliberate, precise strokes. Arrived at empirically — too small produces false direction changes from jitter; too large requires exaggerated gestures.
- Retina-crisp canvas trail: Full-viewport canvas scaled by
devicePixelRatio. Quadratic Bézier smoothing renders the trail as a fluid curve rather than a jagged polyline. - Selective context menu suppression: The right-click context menu fires normally when no gesture is detected. The extension does not hijack right-click globally — only completed gestures suppress the menu.
- Zero network calls, minimal permissions: No host permissions beyond content script injection. No telemetry.
tabsandactiveTabpermissions cover tab switching;scriptingcovers injection.storageis reserved for future gesture configuration. - CSP-compliant MV3 output: Custom
extract-inline.tspost-processor ensures the extension passes Chrome Web Store review without inline script violations.
Implementation Details
Why the 300° Circle Threshold
A 360° threshold would require a perfectly closed circle — achievable with a mouse but uncomfortably precise as a casual gesture. At 300°, the classifier accepts circles that are slightly open (a common natural endpoint when the hand lifts) while rejecting large arcs that are clearly not circular intent. The 80px closure distance constraint prevents long arcs from qualifying as circles by accident.
Bun as Bundler and Build Runtime
Bun's native bundler handles TypeScript without a separate transpilation pass and produces minified output in a single command. The full build from source to loadable dist/ runs in under two seconds, which matters for a development workflow where the iteration loop is: edit content script, rebuild, reload extension in Chrome, test gesture. A slow build (Webpack, for instance, takes 8–15 seconds for a comparable project) would make this loop painful.
Stateless Gesture Classifier Design
Keeping gesture.ts as a pure function with no DOM or browser API dependencies made it directly unit-testable. Direction thresholds, deduplication rules, and circle detection parameters were tuned against a test suite of recorded mouse paths rather than through subjective manual testing. The classifier's output is a string signature ("R,D", "CIRCLE") rather than an action enum, which keeps it decoupled from the action dispatch layer — adding a new gesture requires only a new match arm in the content script's dispatch table.
Tech Stack Rationale
| Component | Technology | Rationale |
|---|---|---|
| Extension Runtime | Chrome MV3 | Current standard; required for Chrome Web Store distribution |
| Language | TypeScript 5 | Strict types across all three runtime contexts; catches cross-context message shape mismatches at compile time |
| Popup UI | Astro | Zero-JS static output by default; avoids inline script CSP violations without custom post-processing for most content |
| Bundler / Build Runtime | Bun | Sub-second builds with native TypeScript support; eliminates separate transpilation step |
| Icon Generation | Sharp | High-quality PNG resizing to 16/32/48/128px without heap bloat; runs as a build step |
| Packaging | adm-zip | Pure-JS ZIP generation; no OS tooling dependency, reproducible on any platform |