npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zappar/zapbox-controller-web

v1.1.0

Published

TypeScript library for Zapbox BLE controllers via Web Bluetooth

Downloads

869

Readme

@zappar/zapbox-controller-web

Use Zapbox BLE controllers from the browser via Web Bluetooth — read their buttons / trigger / thumbstick and get a gravity-corrected 3-DoF orientation from the onboard IMU, with no native app required.

There are two ways to use it:

  • Bookmarklet (no coding). On an Android phone, add Zapbox controller support to a WebXR page someone else built — save a bookmark once, open the page, and tap it. Good for trying existing immersive-vr experiences with Zapbox controllers. See Add controllers to an existing WebXR page.
  • Library (for developers). Integrate Zapbox controllers into your own web project — either as a transparent WebXR shim (existing immersive-vr apps work unchanged) or through the direct controller API (buttons, trigger, thumbstick, orientation). See Use it in your own project.

3-DoF only — orientation, buttons, trigger and thumbstick. There is no camera tracking (6-DoF) in the browser.

Add controllers to an existing WebXR page (bookmarklet)

Android Chrome already provides Google Cardboard-style 3-DoF headset tracking and Web Bluetooth, but it has no extension support — so the shim is delivered as a bookmarklet: a bookmark whose address is a small piece of code instead of a web page. Tapping it on a WebXR page loads the Zapbox shim into that page, and when you enter VR you'll be guided through pairing your controllers.

How to set up (one time, Android Chrome)

  1. Bookmark any page (tap ⋮ followed by the ☆ icon).
  2. Open your bookmarks, edit the one you just made, and replace its URL with the snippet below.
  3. Give it a name with a short, distinctive prefix you can search for, like ZBWX: Add Zapbox Controller Support. You'll find it by searching for ZBWX rather than browsing (see below), so an unusual prefix that won't collide with your history is what matters.
javascript:(function(){var s=document.createElement('script');
s.src='https://cdn.jsdelivr.net/npm/@zappar/zapbox-controller-web@1/dist/zapbox.global.js';
s.onload=function(){window.__zapboxWebXRInstalled||(window.__zapboxWebXRInstalled=Zapbox.installZapboxWebXR())};
document.head.appendChild(s)})();

Viewing this on GitHub? Use the copy button in the snippet's top-right corner to grab the whole thing — handy for pasting straight into the bookmark's URL field on your phone.

How to use it

Open a WebXR page in Chrome, then tap the address bar and type ZBWX (your bookmark's prefix) — the bookmark appears in the suggestions; tap it to run the shim on the current page. Then enter VR as usual — you'll be prompted to pair your controllers first. If no controllers are connected the page is left exactly as it was (headset-only), so it's always safe to run.

Searching the address bar is the reliable way to reach the bookmark: Chrome on Android buries the bookmark list, and neither the address-bar suggestion chips nor the start-page shortcuts let you save a javascript: address — only a normal bookmark can hold one. That's why a distinctive, searchable name matters.

The bookmarklet loads a script into the page, so it won't work on the small number of sites that block external scripts with a strict Content Security Policy. Most WebXR demos are fine.

Use it in your own project

Install

npm install @zappar/zapbox-controller-web

Web Bluetooth only works in a secure context (HTTPS or localhost), and connect() must be called from a user gesture (e.g. a click).

Quick start — a single controller

import { ZapboxController } from '@zappar/zapbox-controller-web';

const controller = await ZapboxController.connect(); // shows the browser's Bluetooth chooser

controller.addEventListener('update', (ev) => {
  const { inputState, imuSamples } = ev.detail;

  // buttons (boolean); trigger 0 to 1; thumbstickX / thumbstickY -1 to +1 (analog)
  const { buttons, trigger, thumbstickX, thumbstickY } = inputState;

  // Primary output — gravity-aligned orientation quaternion, valid once calibrated.
  if (controller.isCalibrated) {
    const q = controller.orientation; // gl-matrix quat [x, y, z, w]
  }
});

// Connection lifecycle (auto-reconnects on unexpected GATT drops / tab visibility changes):
controller.addEventListener('connected', () => {});
controller.addEventListener('reconnecting', () => {});
controller.addEventListener('disconnected', () => {});
// controller.connectionState is 'connected' | 'reconnecting' | 'disconnected'

On connect, place the controller on a stationary surface for a moment: the library collects ~100 stationary samples to calibrate gyro bias and align orientation to gravity. isCalibrated flips to true when that completes.

Input state

inputState.buttons; // { trigger, a, b, menu, grip, thumbstickClick } — all boolean
inputState.trigger; // number, 0 (released) → 1 (fully pressed), auto-calibrated range
inputState.thumbstickX; // number, -1 (left) → +1 (right), 0 in deadzone
inputState.thumbstickY; // number, -1 (down) → +1 (up), 0 in deadzone

Quick start — guided pairing (0–2 controllers)

ZapboxControllerManager drives a guided bottom-sheet pairing UI through connect → confirm → calibrate for one or two controllers, then exposes them as .left / .right.

import { ZapboxControllerManager } from '@zappar/zapbox-controller-web';

const manager = new ZapboxControllerManager({ min: 0, max: 2 });
const count = await manager.setup(); // returns how many were actually connected

manager.left?.addEventListener('update', (ev) => { /* ... */ });
manager.right?.addEventListener('update', (ev) => { /* ... */ });

WebXR shim

import { installZapboxWebXR } from '@zappar/zapbox-controller-web';

installZapboxWebXR(); // patches navigator.xr; no-ops on unsupported environments

installZapboxWebXR(options?) patches navigator.xr so existing immersive-vr WebXR apps see Zapbox controllers as standard input sources with no app changes. It no-ops unless the environment is supported (phone-class Android Chrome + Web Bluetooth), so it's safe to inject unconditionally. On the first requestSession it runs guided pairing and a short calibration pre-roll. If zero controllers are connected it returns the unmodified session (pure Cardboard passthrough — controllers are progressive enhancement). Options: { min?, max?, recommended?, connectionInterval?, includeNativeInputSources?, armModel?, patchChromeNeckModel?, forceNativeFramebufferScale?, showAdvancedOptions? } (defaults min: 0, max: 2, armModel: true, patchChromeNeckModel: true, forceNativeFramebufferScale: false, showAdvancedOptions: true). forceNativeFramebufferScale forces the page's WebXR layer to render at native resolution, overriding even an explicit scale factor — Chrome mobile defaults below native, so this is a sharpness win (aimed at bookmarklet use over pages you can't edit; only applies once controllers are connected). showAdvancedOptions adds an "Advanced settings" link to the setup flow's final "Enter VR" screen so the user can toggle armModel / patchChromeNeckModel / forceNativeFramebufferScale before entering VR — the only way to reach them under bookmarklet use; pass false to hide it. The passed options are merged into an exported mutable object zapboxWebXRLaunchOptions, read live at each session start — mutate it at runtime to change behaviour on the next "Enter VR", and read it back to see what the user chose in the Advanced panel.

The controllers map to a standard xr-standard gamepad: buttons[0] trigger (analog on .value), buttons[1] grip, buttons[3] thumbstick click, buttons[4] Menu, buttons[5] A, buttons[6] B; thumbstick on axes[2] / axes[3].

Browser global (no bundler)

For <script src> use, the package ships a self-contained minified IIFE that exposes window.Zapbox with the same API surface as the module entry point (bundles the library + gl-matrix). This is also what the bookmarklet loads:

<script src="https://cdn.jsdelivr.net/npm/@zappar/zapbox-controller-web@1/dist/zapbox.global.js"></script>
<script>
  // window.Zapbox.{ ZapboxController, ZapboxControllerManager, installZapboxWebXR, ... }
  Zapbox.installZapboxWebXR();
</script>

License

MIT © Zappar Limited