Blog Protocol 8 min read

WebMIDI in 2026 — Browser-Only Gamepad Rig

Run a browser-only DualSense to MIDI bridge with no driver install. Works in Chromium and Firefox — Safari does not ship the Web MIDI API.

By Aidxn Design

WebMIDI gamepad rigs were impossible to take seriously in 2017. The API existed in Chrome but no other browser implemented it, Safari refused to ship, and you couldn't open a sysex channel without a flag. In 2026 every Chromium browser ships WebMIDI by default and Firefox supports it behind a site-permission add-on, though Safari still does not implement it at all. The Gamepad API has been stable since 2014. Put them together and you can drive a hardware synth, Ableton, or a WebGL VJ tool from a DualSense — with zero install. This post is the state of the art.

TL;DR
  • Browser support: Chrome 43+, Edge 79+, Opera 30+, Firefox 108+ (site-permission add-on). Not Safari, on any platform.
  • Permission: WebMIDI asks once per origin. Sysex needs an explicit opt-in.
  • Latency cost: dominated by the Gamepad API's once-per-frame polling, not by the MIDI call. Live-usable on Chromium.
  • Gamepad polling: requestAnimationFrame at 60 Hz (~16.7 ms). Diff-and-send to avoid CC spam.
  • The catch: backgrounded tabs throttle. Keep the tab visible during performance.

What WebMIDI actually exposes

The W3C WebMIDI spec exposes a single asynchronous entry point: navigator.requestMIDIAccess(). It resolves with a MIDIAccess object containing two maps — inputs for devices the browser can read from, and outputs for devices it can write to. Both maps fire statechange events when devices appear or disappear, so you can hot-plug a controller without reloading the page.

Sending a MIDI message is one line: output.send([0x90, 60, 100]) dispatches a Note On for C4 at velocity 100. There is an optional second argument for scheduled timestamps, which is the bit nobody mentions but matters enormously — output.send(data, performance.now() + 50) queues the message 50 ms in the future, giving you sample-accurate timing without busy-waiting in JavaScript.

// Minimal WebMIDI + Gamepad rig — no library, runs in any modern browser
const access = await navigator.requestMIDIAccess({ sysex: false });
const output = [...access.outputs.values()][0];   // first MIDI out

let lastState = [];
function tick() {
    const pads = navigator.getGamepads();
    const gp = pads[0];
    if (gp) {
        // Send CC 16 from left stick X, CC 17 from left stick Y
        const x = Math.round((gp.axes[0] + 1) * 63.5);   // -1..1 → 0..127
        const y = Math.round((gp.axes[1] + 1) * 63.5);
        if (x !== lastState[0]) output.send([0xB0, 16, x]);
        if (y !== lastState[1]) output.send([0xB0, 17, y]);
        lastState = [x, y];
    }
    requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

Browser support, broken down

WebMIDI was a Chromium-only party for years. The 2022–2025 stretch changed that. As of June 2026:

BrowserVersion landedSysexNotes
Chrome / Edge43 (Apr 2015)YesReference implementation. Most stable.
Opera30 (Jul 2015)YesSame engine as Chrome.
Firefox108 (Dec 2022)YesRequires Permissions-Policy header on iframes.
Safari (macOS, iPadOS)—NoNot implemented in any shipping version.
Safari (iPhone)—NoNot implemented. All iOS browsers use the same engine.
Brave / Vivaldi / ArcInherit ChromiumYesNo additional flags.

The permission prompt

WebMIDI is gated by a one-time prompt per origin. Without sysex, the prompt reads "Allow this site to control MIDI devices?" — once approved, the permission persists indefinitely. With sysex, the prompt explicitly warns about firmware writes, which is correct because sysex can flash device firmware. The bridge's browser playground only requests sysex when you tick the sysex checkbox, so casual visitors never see the scary prompt.

Gamepad API + WebMIDI — the loop

The W3C Gamepad API is poll-based — there is no "axis changed" event. You call navigator.getGamepads() once per frame, diff against the previous frame, and emit MIDI messages for changes. Polling rate matters:

  • requestAnimationFrame: 60 Hz on most displays, 120 Hz on ProMotion/144Hz panels. Default choice.
  • setInterval(fn, 4): 250 Hz polling. Smoother but burns CPU and won\'t outrun the gamepad\'s own internal rate.
  • Web Worker + OffscreenCanvas: keeps polling alive when the tab is backgrounded. Hack of last resort.

Gotchas you will hit

Six things that bit us building the playground:

  1. Backgrounded tabs throttle to 1 Hz. Switch tabs mid-performance and your gamepad input freezes. Keep the tab visible.
  2. The first gamepad button press is required. Chromium hides connected gamepads until any input arrives — fire a button on launch.
  3. Safari uses a different standard layout. Triggers on Safari are axes[6] and axes[7]; on Chrome they're buttons[6] and buttons[7]. Detect and branch.
  4. Output buffers when the tab loses focus. Queued messages flush when the tab returns, which can produce a stuck-note burst. Flush before blur.
  5. Sysex over WebMIDI is chunked to ~256 bytes. Larger dumps must be split. See sysex preset transfer for the chunking pattern.
  6. HTTPS is required. WebMIDI refuses to load on http://. localhost is exempt.

Latency — browser vs native

An earlier version of this page published a benchmark table comparing native, Chrome and Safari transports with median and p95 figures. Those numbers have been removed: the table included a row for Web MIDI in Safari, and Safari does not implement the Web MIDI API on any platform, so that measurement cannot have been taken. Rather than leave figures of uncertain provenance on the page, here is the mechanism, which is the part that actually helps you.

In a browser rig the MIDI call is rarely the bottleneck. The Gamepad API has no input events — you read a snapshot of controller state, normally once per animation frame. At 60 Hz that is a frame every ~16.7 ms, so a button press waits up to one frame before your code even observes it. That quantisation dominates anything the send() call itself contributes.

Three things move the number far more than which browser you picked: whether the tab is visible (background tabs are throttled hard), what else is competing for the main thread (a heavy canvas or a busy React tree will stall your polling loop), and whether you schedule messages with a timestamp instead of sending them the instant you notice a change.

For measured figures on the native path, see the gamepad MIDI latency benchmark, which documents its own method and rig. Treat those as this site's measurements on this site's hardware, not as universal constants.

When the browser rig actually wins

Native always wins on raw latency. The browser rig wins everywhere else:

  • Zero install. Open the URL, hit a button, you\'re live. Massive for workshops, classrooms, and demos.
  • Sharable presets. URL-encode the mapping and you can DM a friend a link that loads their exact rig.
  • Embedded VJ tools. WebGL visualisers can subscribe to your CC stream and react in the same page, no IPC needed.
  • Education. Inspecting output.send in DevTools is a faster way to teach MIDI than reading the spec.

Try the browser playground or read about the cross-protocol comparison in OSC vs MIDI. For the desktop install path, head back to Universal Controller MIDI.

FAQ

Which browsers support WebMIDI in 2026?

Chromium browsers (Chrome 43+, Edge 79+, Opera 30+) ship it by default, and Firefox 108+ supports it behind a site-permission add-on. Safari does not support the Web MIDI API on macOS, iPadOS or iOS in any shipping version — check caniuse.com/midi for the current table.

Does WebMIDI need a browser extension?

No, not since Chrome 43 (April 2015). Sysex permission still requires explicit user approval, but the basic CC + Note path needs zero extensions.

Can WebMIDI talk to hardware synths directly?

Yes. The browser writes to the same MIDI device list your DAW sees — IAC buses, USB MIDI interfaces, BLE MIDI peripherals, and RTP-MIDI sessions all show up in MIDIAccess.outputs.

What's the latency penalty for staying in the browser?

The dominant cost is not the MIDI call itself but the Gamepad API polling model: you read controller state once per animation frame, so at 60 Hz a press waits up to one frame before it is even noticed. Main-thread contention and background-tab throttling matter more than the browser brand.

Does WebMIDI work on iOS?

No. Safari does not implement the Web MIDI API on iOS or iPadOS, and because every iOS browser is required to use Safari's engine, no iOS browser offers it. On iOS you need a native app.

How is gamepad input combined with WebMIDI?

The Gamepad API (navigator.getGamepads) runs alongside WebMIDI. The bridge polls gamepad state at requestAnimationFrame, diffs it, and writes MIDI messages to the chosen output port.

Keep reading

More setup walkthroughs