Why relative mouse input matters in browser games
First-person shooter controls in a browser game depend on relative mouse movement. Without it, a player moving the cursor to the right edge of the screen would hit a wall: further rightward motion would be discarded, the view would stop panning, and the player would lose the ability to track targets. The Pointer Lock API solves this by granting access to raw mouse data and routing all mouse events to a specific element instead of the browser viewport.
A side effect of pointer lock is that the mouse cursor disappears. You can draw your own in-game cursor, or leave it hidden entirely to let the mouse drive camera movement without a visible pointer. Relative mouse movement is the delta from the previous mouse position. For instance, a move from (640, 480) to (520, 490) produces a relative delta of (-120, 10).
The API is supported across modern Chrome, Firefox, Safari, and Edge browsers.
Enabling and tracking pointer lock
Feature detection and activation
Before using pointer lock, check that the browser exposes pointerLockElement (or a vendor-prefixed variant) on the document object:
var havePointerLock = 'pointerLockElement' in document ||
'mozPointerLockElement' in document ||
'webkitPointerLockElement' in document;
Activation is a two-step process. The application requests pointer lock on a specific element, and once the user grants permission, a pointerlockchange event fires. The user can exit pointer lock at any time with the escape key; your code can also release it programmatically. Both exit paths trigger another pointerlockchange event. Locking the pointer also shows a browser notification explaining the escape key behavior.
Requesting pointer lock requires only a call on the target element
element.requestPointerLock = element.requestPointerLock ||
element.mozRequestPointerLock ||
element.webkitRequestPointerLock;
// Ask the browser to lock the pointer
element.requestPointerLock();
// Ask the browser to release the pointer
document.exitPointerLock = document.exitPointerLock ||
document.mozExitPointerLock ||
document.webkitExitPointerLock;
document.exitPointerLock();
Handling state changes and mouse movement
Two event listeners matter: pointerlockchange for lock state transitions, and mousemove for tracking motion:
// Hook pointer lock state change events
document.addEventListener('pointerlockchange', changeCallback, false);
document.addEventListener('mozpointerlockchange', changeCallback, false);
document.addEventListener('webkitpointerlockchange', changeCallback, false);
// Hook mouse move events
document.addEventListener("mousemove", this.moveCallback, false);
In the pointerlockchange callback, compare document.pointerLockElement against the element you locked to determine whether the lock is active or has been released:
if (document.pointerLockElement === requestedElement ||
document.mozPointerLockElement === requestedElement ||
document.webkitPointerLockElement === requestedElement) {
// Pointer was just locked
// Enable the mousemove listener
document.addEventListener("mousemove", this.moveCallback, false);
} else {
// Pointer was just unlocked
// Disable the mousemove listener
document.removeEventListener("mousemove", this.moveCallback, false);
this.unlockHook(this.element);
}
Once pointer lock is active, the values of clientX, clientY, screenX, and screenY stay constant. What updates instead are the relative movement fields:
event.movementX = currentCursorPositionX - previousCursorPositionX;
event.movementY = currentCursorPositionY - previousCursorPositionY;
Inside the mousemove handler, pull the per-event delta from movementX and movementY:
function moveCallback(e) {
var movementX = e.movementX ||
e.mozMovementX ||
e.webkitMovementX ||
0,
movementY = e.movementY ||
e.mozMovementY ||
e.webkitMovementY ||
0;
}
The pointerlockerror event fires if either entering or exiting pointer lock raises an error. This event carries no additional data, and you would attach a listener in the usual way:
document.addEventListener('pointerlockerror', errorCallback, false);
document.addEventListener('mozpointerlockerror', errorCallback, false);
document.addEventListener('webkitpointerlockerror', errorCallback, false);
Pointer lock no longer requires the FullScreen API. Any element can have the pointer locked to it, whether or not it is in fullscreen.
Quake-style controls from three vectors
With pointer lock handling input, building a first-person camera comes down to a small set of mechanics:
- Move forward and backward along the current look direction
- Strafe left and right along the horizontal perpendicular
- Rotate the view horizontally (yaw)
- Rotate the view vertically (pitch)
The implementation needs only three datapoints: camera position, camera look vector, and a constant up vector of (0, 1, 0). Every movement and rotation mechanic modifies just the position and look vector.
Movement and strafing
In the standard WASD scheme, W and S move the camera forward and back along the look vector:
// Forward direction
var forwardDirection = vec3.create(cameraLookVector);
// Speed
var forwardSpeed = dt * cameraSpeed;
// Forward or backward depending on keys held
var forwardScale = 0.0;
forwardScale += keyState.W ? 1.0 : 0.0;
forwardScale -= keyState.S ? 1.0 : 0.0;
// Scale movement
vec3.scale(forwardDirection, forwardScale * forwardSpeed);
// Add scaled movement to camera position
vec3.add(cameraPosition, forwardDirection);
A and D strafe along the perpendicular direction, which you can obtain via the cross product of the look vector and the up vector:
// Strafe direction
var strafeDirection = vec3.create();
vec3.cross(cameraLookVector, cameraUpVector, strafeDirection);
With the strafe direction computed, left and right movement follows the same pattern as forward and back.
Yaw and pitch
Yaw is a rotation of the look vector around the constant up vector. A general rotation around any axis builds a quaternion of deltaAngle radians around that axis and applies it to the look vector:
// Extract camera look vector
var frontDirection = vec3.create();
vec3.subtract(this.lookAtPoint, this.eyePoint, frontDirection);
vec3.normalize(frontDirection);
var q = quat4.create();
// Construct quaternion
quat4.fromAngleAxis(deltaAngle, axis, q);
// Rotate camera look vector
quat4.multiplyVec3(q, frontDirection);
// Update camera look vector
this.lookAtPoint = vec3.create(this.eyePoint);
vec3.add(this.lookAtPoint, frontDirection);
Pitch applies the same kind of rotation, but around the strafe vector instead of the up vector. So you first compute the strafe vector as above, then rotate the look vector around that axis for vertical aiming.
What to remember
Using pointer lock in a web game comes down to three steps: add a pointerlockchange listener to track lock state, request pointer lock on a chosen element, then read movement from a mousemove listener. The result is a camera control scheme that behaves like a native desktop title, without the browser losing track of the mouse at viewport edges. External demonstrations, including a Quake 3 map viewer and documentation on the Mozilla Developer Network, provide further reference material for games using pointer lock.



