01Brief
I’d been hooked on Baldur’s Gate 3 and had a trip coming up. The plan sounded simple: keep playing on Xbox Cloud Gaming from my MacBook, using the Joy-Con from my Nintendo Switch 2 as controllers.
It wasn’t. The original Joy-Con showed up as standard Bluetooth gamepads. The Switch 2 Joy-Con speak a proprietary Bluetooth LE protocol with their own pairing, so macOS can’t pair them, and even once connected it has no idea what their data means. The closest fix I found was JoyCon2Mac, a native app that already had the protocol working. But it relies on a system driver that, without entitlements from Apple, only loads with System Integrity Protection and AMFI turned off. Rebooting into recovery mode to play a game felt like a bit much.
Chrome talks to Bluetooth LE devices through Web Bluetooth, and games read controllers through the Gamepad API. Joy2Cloud lives in between: it connects both Joy-Con from a control tab, decodes their reports, and makes the game page see a regular Xbox controller, with buttons, sticks, stick calibration and vibration. I designed, built, tested and published it on my own, and it’s open source under the MIT license.
None of it would exist without the people who reverse engineered these controllers: ndeadly’s switch2_controller_research for the protocol, and JoyCon2Mac for the rumble encoding and the original idea.
I just wanted to play Baldur’s Gate 3 on vacation. I ended up shipping a Chrome extension instead.
02Constraints
Operating system
Nothing installed on macOS and nothing disabled. The existing native fix needed System Integrity Protection and AMFI turned off.
Protocol
Switch 2 Joy-Con speak a proprietary Bluetooth LE protocol, not standard HID, and reject the normal pairing a computer tries.
Browser
Extension service workers can't use Web Bluetooth, and Chrome throttles timers in background tabs to once per second.
Team
Just me, building it to play on a trip.
03Architecture
Input
Vibration
04Decisions
D-01
Put the bridge in the browser, not in the OS
Why
Cloud gaming already runs in the browser. Web Bluetooth reads the controllers and the Gamepad API is how the game reads them back, so the whole path fits inside an extension.
Trade-off
Chrome and other Chromium browsers only (Safari and Firefox have no Web Bluetooth), and it only helps games that run in a browser tab.
D-02
Show the game a virtual Xbox controller
Why
A script in the page overrides navigator.getGamepads() with a standard Xbox mapping and a vibration actuator. Every service reads controllers that way, so there is no per-site code.
Trade-off
The script has to load before the page does, and each service still needs to be checked by hand.
D-03
A control tab owns the Bluetooth connection
Why
Service workers can't use Web Bluetooth and Chrome anchors the device chooser to the address bar, so a regular tab holds both Joy-Con and merges them into one gamepad.
Trade-off
The control tab has to stay open while you play, even if in the background.
D-04
Send vibration with each controller report, not on a timer
Why
A background tab only gets one timer tick per second. Joy-Con reports keep arriving, so riding on them keeps rumble in sync with the game.
Trade-off
The Switch 2 HD rumble format isn't fully documented, so vibration is an approximation of what the console does.
05Outcome
0
Drivers installed or security settings changed
1
Browser permission requested (storage)
4
Interface languages
90s
Auto-reconnect window
06What I'd change
I only tested it on Xbox Cloud Gaming before release. GeForce NOW, Amazon Luna and Boosteroid read controllers through the same Gamepad API, so they should work, but "should" isn't a test. Next time I'd ship with a real compatibility matrix and an easy way for people to report results from day one.