The swap, stated exactly
Nintendo arranges its face-button letters in the mirror of Microsoft's. Because the
Gamepad API numbers positions rather than letters, the index follows the place and the
letter moves. On a Pro Controller the button marked A is
buttons[1] where an Xbox pad has B; the button marked
B is buttons[0] where an Xbox pad has A. X and Y
swap the same way.
| Index | Position, per the specification | Switch Pro | Xbox | PlayStation |
|---|---|---|---|---|
| buttons[0] | Bottom button in right cluster | B | A | Cross ✕ |
| buttons[1] | Right button in right cluster | A | B | Circle ○ |
| buttons[2] | Left button in right cluster | Y | X | Square □ |
| buttons[3] | Top button in right cluster | X | Y | Triangle △ |
Across all 17 canonical indices, a Pro Controller and an Xbox pad print the same thing at exactly 4 of them — and all 4 are the d-pad directions, where an arrow is an arrow on both. Every other index carries a different label. That is the strongest possible argument against printing a button letter from an index without first checking which controller you are holding.
The browser's source says so outright
This is unusually well evidenced for a mapping question, because Chromium decodes Switch
controllers with dedicated code rather than a generic remap — so instead of shuffling
anonymous raw indices, its source assigns canonical positions from named fields.
The function is UpdateGamepadFromControllerData in nintendo_controller.cc, and these are
its assignments for the face cluster.
| Canonical index | Assigned from | Chromium's comment |
|---|---|---|
| buttons[0] | data.button_b | // B button |
| buttons[1] | data.button_a | // A button |
| buttons[2] | data.button_y | // Y button |
| buttons[3] | data.button_x | // X button |
Read the first row: canonical index 0 — the bottom button of the right cluster — is filled
from the controller's B field, and the browser's own comment on the matching entry says
"// B button". There is no ambiguity to resolve and no bug to report. A game that prints
"press A" for buttons[0] is simply wrong on this hardware, and the fix
belongs in the game.
What to check on a Pro Controller specifically
- All four face buttons, naming them out loud as you press so the swap does not confuse the result: press A and expect index 1 to light.
- D-pad, all four directions. These are the 4 indices that agree with every other pad.
- L, R, ZL, ZR. Note that ZL and ZR sit at the canonical trigger positions but are digital switches on this controller — the value jumps straight across with no sweep, which is correct here and would be a fault on an Xbox pad. Compare with the analog trigger test if you are unsure what a sweep looks like.
- Minus, Plus, HOME and Capture. Capture appears at index 17, in the extras row, past the canonical 17.
- Both stick clicks, then both sticks at rest for a minute on the drift meter.
Why ZL and ZR behave unlike Xbox triggers
On an Xbox or PlayStation pad, canonical indices 6 and 7 are analog: the value climbs continuously as you squeeze, and the pressed flag flips somewhere along the way. The Pro Controller's ZL and ZR occupy the same canonical positions but are switches, so the value goes from 0 to 1 with nothing in between. Neither behaviour is a fault; the canonical layout describes where an input is, not what kind of sensor sits behind it. This is exactly why code should tolerate either answer at those indices and never assume a continuous value is available.
If the controller does not appear at all
Press a button first — a connected pad is invisible to the page until you use one, by
design. If it still does not appear, the most productive next step is to change the
connection: Chromium's handling of Switch controllers differs by path, including whether a
device is presented as one composite pad or as separate halves. Compare the button count
and the id string in the device panel between USB and Bluetooth before
concluding anything about the hardware, and check the same pad in a second browser to
separate a device-table gap from a real fault.