Controller button mapping, index by index

The Gamepad API numbers positions on a controller, not the letters printed on it. This page lays out all 17 canonical indices with the specification's own wording beside the button that physically sits there on four popular pads — and then lets you read your own controller's indices directly, which is the only answer that is certainly right.

No controller yet

Press any button on your controller

A connected pad stays invisible to this page until you use it. That is the specification working as designed, not a fault — see the note below the tool.

Device report

id
waiting for input
mapping
—
index
—
buttons
—
axes
—
vibrationActuator
—

Vibration

Buttons

  • [0] Face button, bottom—
  • [1] Face button, right—
  • [2] Face button, left—
  • [3] Face button, top—
  • [4] Left shoulder (digital)—
  • [5] Right shoulder (digital)—
  • [6] Left trigger (analog on most pads)—
  • [7] Right trigger (analog on most pads)—
  • [8] Select-side centre button—
  • [9] Start-side centre button—
  • [10] Left stick click—
  • [11] Right stick click—
  • [12] D-pad up—
  • [13] D-pad down—
  • [14] D-pad left—
  • [15] D-pad right—
  • [16] Vendor / guide button—

Axes

  • [0] left stick horizontal0.000
  • [1] left stick vertical0.000
  • [2] right stick horizontal0.000
  • [3] right stick vertical0.000

The complete index table

The Position column is quoted from the button table in the W3C Gamepad specification, and the Chromium column gives the name the browser's own code uses for the same index — declared in the same order, which is how you can see the two documents agree. The four controller columns are this site's contribution; the specification never names a vendor.

Standard Gamepad indices 0–16. Highlighted cells differ from the Xbox column.
Index Position, in the specification's own words Chromium's name XboxDualSense (PS5)DualShock 4 (PS4)Switch Pro
buttons[0] Bottom button in right cluster BUTTON_INDEX_PRIMARY ACross ✕Cross ✕B
buttons[1] Right button in right cluster BUTTON_INDEX_SECONDARY BCircle ○Circle ○A
buttons[2] Left button in right cluster BUTTON_INDEX_TERTIARY XSquare □Square □Y
buttons[3] Top button in right cluster BUTTON_INDEX_QUATERNARY YTriangle △Triangle △X
buttons[4] Top left front button BUTTON_INDEX_LEFT_SHOULDER LBL1L1L
buttons[5] Top right front button BUTTON_INDEX_RIGHT_SHOULDER RBR1R1R
buttons[6] Bottom left front button BUTTON_INDEX_LEFT_TRIGGER LTL2L2ZL
buttons[7] Bottom right front button BUTTON_INDEX_RIGHT_TRIGGER RTR2R2ZR
buttons[8] Left button in center cluster BUTTON_INDEX_BACK_SELECT ViewCreateShareMinus −
buttons[9] Right button in center cluster BUTTON_INDEX_START MenuOptionsOptionsPlus +
buttons[10] Left stick pressed button BUTTON_INDEX_LEFT_THUMBSTICK LS (left stick click)L3L3Left stick click
buttons[11] Right stick pressed button BUTTON_INDEX_RIGHT_THUMBSTICK RS (right stick click)R3R3Right stick click
buttons[12] Top button in left cluster BUTTON_INDEX_DPAD_UP D-pad ↑D-pad ↑D-pad ↑D-pad ↑
buttons[13] Bottom button in left cluster BUTTON_INDEX_DPAD_DOWN D-pad ↓D-pad ↓D-pad ↓D-pad ↓
buttons[14] Left button in left cluster BUTTON_INDEX_DPAD_LEFT D-pad ←D-pad ←D-pad ←D-pad ←
buttons[15] Right button in left cluster BUTTON_INDEX_DPAD_RIGHT D-pad →D-pad →D-pad →D-pad →
buttons[16] Center button in center cluster BUTTON_INDEX_META Xbox buttonPS buttonPS buttonHOME
The four canonical axes. Both sticks report a pair; the vertical axes are positive downward, which catches out nearly everyone the first time.
Index Position, in the specification's own words −1 means +1 means
axes[0] Horizontal axis for left stick (negative left/positive right) left right
axes[1] Vertical axis for left stick (negative up/positive down) up down
axes[2] Horizontal axis for right stick (negative left/positive right) left right
axes[3] Vertical axis for right stick (negative up/positive down) up down

The Nintendo problem, stated precisely

Nintendo prints its face-button letters in the opposite arrangement to Microsoft. Because the API numbers positions, the letters move with the position rather than the index. On an Xbox controller the button marked A is buttons[0]; on a Switch Pro Controller the button marked A is buttons[1]. B is buttons[1] on Xbox and buttons[0] on the Pro Controller. The two pads print the same thing at exactly 4 of the 17 canonical indices, and all 4 are the d-pad directions (D-pad ↑, D-pad ↓, D-pad ←, D-pad →).

This is not the browser being sloppy — it is the browser being consistent. Chromium's Nintendo mapping code assigns BUTTON_INDEX_PRIMARY from the controller's button_b field, and the neighbouring array literal comments the four entries "// B button", "// A button", "// Y button", "// X button" in that order. The browser has decided, correctly, that the position is what the index means. The consequence for anyone shipping software is blunt: never print a face-button letter from an index unless you have first read the controller's id and decided which letter set applies. The Switch Pro controller test shows the source that settles it, and the Xbox controller tester is the other half of the comparison if you have both pads to hand.

Where the two PlayStation pads disagree

The DualSense and the DualShock 4 are the easy case. Across all 17 canonical indices they differ at exactly 1: buttons[8], the left button in center cluster, where Sony renamed the DualShock 4's Share button to Create on the DualSense. The position, the index and the behaviour are identical; only the printing changed. Code written against indices needs no changes between the two generations, which is precisely the payoff the canonical layout exists to deliver. The PS4 controller test takes that further and checks the browser's own mapper source for both pads.

Index 17: the buttons that do not fit

The Standard Gamepad ends at 16, and real controllers kept adding buttons afterwards. Chromium handles this by appending the extra input immediately after the canonical set, so on each of these devices the extra button lands at index 17. The enum names below are the identifiers in Chromium's source, and the last column names the mapper files where the assignment is actually written — which is not the same question as where the name is declared.

Buttons past the canonical set. Each enum value is declared as BUTTON_INDEX_COUNT in gamepad_standard_mappings.h — three of them bare, the Xbox one via the qualified CanonicalButtonIndex::BUTTON_INDEX_COUNT — and BUTTON_INDEX_COUNT is 17.
IndexControllerButtonChromium enumAssigned in
buttons[17] Xbox Series X|S controller Share XBOX_SERIES_X_BUTTON_SHARE gamepad_standard_mappings_mac.mm, gamepad_standard_mappings_linux.cc
buttons[17] Switch Pro Controller Capture SWITCH_PRO_BUTTON_CAPTURE gamepad_standard_mappings_win.cc, gamepad_standard_mappings_mac.mm, gamepad_standard_mappings_linux.cc
buttons[17] DualSense (PS5) Touchpad click DUAL_SENSE_BUTTON_TOUCHPAD gamepad_standard_mappings_win.cc, gamepad_standard_mappings_mac.mm, gamepad_standard_mappings_linux.cc
buttons[17] DualShock 4 (PS4) Touchpad click DUALSHOCK_BUTTON_TOUCHPAD gamepad_standard_mappings_win.cc, gamepad_standard_mappings_mac.mm, gamepad_standard_mappings_linux.cc

The last column is where the surprise lives. Three of these four are assigned on all three desktop platforms; the Xbox Share button is assigned only by the macOS and Linux mappers, in a function that runs after the ordinary Xbox Bluetooth mapper and raises the reported button count. Chromium's Windows mapper has no equivalent row, so a Series X|S pad does not report the same button count everywhere. The Linux file does not even reuse the shared name — it declares its own constant at the same value — which is exactly why the column says where the assignment happens instead of leaving you to infer it from an enum.

Notice too that all four rows share an index. Index 17 alone tells you nothing about which button was pressed — it is a touchpad click on one pad and a capture button on another. The only way to disambiguate is the controller's id string, which the device panel in the tool prints in full. This also explains a reading you may already have seen: a pad reporting 18 buttons where you expected 17 is not broken and is not "extra standard"; it is a standard pad plus one appended extra.

When the table does not apply at all

Everything above is conditional on your controller reporting mapping === "standard". The property is an enumeration with three legal values and no others.

GamepadMappingType, from the specification's own enum declaration.
ValueSpecificationConsequence
"standard" The user agent has remapped the device onto the Standard Gamepad layout, so the index table on this page applies. Indices are portable. A game that reads buttons[0] gets the bottom face button on every pad reporting this value.
"" (empty string) The spec states plainly: “The empty string indicates that no mapping is in use for this gamepad.” Indices are whatever the device reports in its own HID order. The table on this page does not apply, and neither does anyone else’s. Read the labels from the tester, not from a chart.
"xr-standard" Reserved for WebXR input sources, and never for ordinary game controllers. getGamepads() must never report it — the specification says so outright. XR controllers reach a page through WebXR instead, not through this API.

If yours reports the empty string, the fastest route to a correct mapping is to build your own in about thirty seconds: open the live panel, press each button in a fixed order, and write down the index that lights. That list is authoritative for your device in a way no published chart can be. It is also worth repeating the exercise if you use the pad through an adapter, because the adapter — not the controller — is what the browser identifies, and two adapters for the same controller can produce two different orderings.

Advice for people writing code against this

  • Bind by index, label by id. Read buttons[0], but decide whether to draw "A", "Cross" or "B" from the device string.
  • Check mapping before trusting an index. One equality test separates the devices your layout describes from the ones it does not.
  • Do not assume 17. Iterate buttons.length; extras start at index 17, are device-specific, and on the Xbox Series X|S are platform-specific as well.
  • Remember the vertical axes point down. axes[1] and axes[3] are positive downward, which the specification states outright and which inverts roughly half of all first drafts.
  • Read value, not pressed, at indices 6 and 7. The flag is a convenience with a browser-chosen threshold baked into it; apply your own trigger dead zone and let the player set it. And do not assume those two indices are analog at all — the analog trigger test shows what a digital one looks like.
  • Offer a remap screen anyway. Every assumption above fails for some real device; a rebinding UI is the only complete answer.

Checking your own pad against the table

The live panel above shows each canonical index with its value, so you can verify the table against your hardware in a minute. Press the bottom face button and confirm index 0 lights. Push the left stick right and confirm axes[0] goes positive; push it down and confirm axes[1] goes positive too — that is the one that surprises people. Then squeeze each trigger slowly and watch buttons[6] and buttons[7] climb continuously, with no snap straight to 1, which is the subject of the analog trigger test. If any of these disagree with the table, check mapping first — it explains the overwhelming majority of mismatches.

Questions people actually ask

Which button is buttons[0]?

The specification defines it as the "Bottom button in right cluster" — the position, not a letter. That is A on an Xbox controller, Cross on a PlayStation controller, and B on a Switch Pro Controller. Chromium's source makes the Nintendo case explicit: it assigns buttons[BUTTON_INDEX_PRIMARY] from the controller's B button and comments the entry "// B button".

Why is there no official table with Xbox and PlayStation names in it?

Because the specification deliberately avoids vendor names. It describes one canonical layout in geometric terms so that any device — including controllers that did not exist when it was written — can be mapped onto it. The vendor columns on this page are this site's work: the specification supplies the position, and the product supplies which button sits there.

What is index 17, and why do different controllers disagree about it?

The Standard Gamepad stops at index 16, but real controllers have buttons that do not fit it — a touchpad click, a capture button, a share button. Chromium appends those immediately after the canonical set, so each lands at index 17 on its own device. The table on this page lists the four cases named in Chromium's source, alongside the mapper files that assign each one; because they all occupy the same index, you cannot tell them apart by index alone, only by the controller's id.

My pad reports mapping as an empty string. Does this table still apply?

No, and that is the important thing to take away. An empty string means, in the specification's words, that no mapping is in use for this gamepad — the indices are whatever order the device reports internally. No published chart can be trusted in that state, including this one. Use the live panel to read your own indices instead; it takes about thirty seconds to press every button and write down what lights up.

Are the D-pad directions buttons or an axis?

Under the Standard Gamepad they are four separate buttons, indices 12 to 15. Plenty of hardware reports a d-pad as a "hat" axis instead, and the browser converts it during remapping — which is one reason a pad that reports a non-standard mapping may show d-pad presses land on an axis while those four indices stay dark.

Does the same index mean the same thing in Firefox and Safari?

For a pad reporting "standard", that is what the value is supposed to promise. This site does not go further than that, because we read Chromium's mapping source and did not trace Firefox's or Safari's to a conclusion. Where a figure on this site comes from Chromium specifically, it carries a Chromium label; nothing here presents it as browser-wide behaviour.

Keep testing

Test the rest of the desk