Quick answer: D-pads vary: some are hats, some are buttons, some are an axis pair. Use the SDL2 Controller API (pygame._sdl2.controller) for a normalized D-pad.

A menu uses the D-pad for navigation. It works on an Xbox pad but not a generic USB controller — that one exposes the D-pad differently.

The Three D-Pad Styles

Use the Controller API

from pygame._sdl2 import controller

controller.init()
pad = controller.Controller(0)

# normalized button constants regardless of hardware
if pad.get_button(controller.CONTROLLER_BUTTON_DPAD_UP):
    menu_up()

SDL2’s GameController layer maps every known pad’s D-pad to the same constants. This is the robust fix.

Raw Joystick Fallback

If you must use the raw Joystick API, handle all three:

if event.type == pygame.JOYHATMOTION:
    x, y = event.value
elif event.type == pygame.JOYBUTTONDOWN:
    # check known D-pad button indices per controller
    pass

Maintain a per-controller mapping table — tedious, which is why the Controller API exists.

Verifying

Test on Xbox, PlayStation, and a generic USB pad. D-pad navigation works on all three with the same code path.

“D-pad hardware varies wildly. The SDL2 Controller API normalizes it — use it.”

Ship an SDL_GameControllerDB file with your game so even obscure controllers get a correct mapping.