| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
parent directory.. | ||||
🤖 AI coding agents: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository README and AGENTS.md, then use the wcstack-app skill.
@wcstack/pointer-lock is a headless Pointer Lock API component for the wcstack ecosystem.
It is not a visual UI widget. It is an async primitive node that turns the Pointer Lock API's lock state into reactive state — the same way @wcstack/fullscreen turns the Fullscreen API's active state into reactive state.
With @wcstack/state, <wcs-pointer-lock> can be bound directly through path contracts:
@wcstack/pointer-lock follows the CSBC (Core / Shell / Binding Contract) architecture:
Unlike most wcstack IO nodes, <wcs-pointer-lock> is not aimed at the project's main use case of building declarative SPA UI. The Pointer Lock API's real-world usage is almost exclusively games and canvas/WebGL rendering UI that need the mouse's relative movement (movementX/movementY) — first-person camera controls, drawing-tool panning, and similar. Those consumers typically already run an imperative requestAnimationFrame loop and have less reason to route input through a declarative binding layer than, say, a video player reaching for <wcs-fullscreen>.
Reach for this node when you need a declarative lock on/off switch (e.g. a "Enable mouse look" button wired via the command-token protocol) — not as a source of movementX/movementY data. See below.
mousemove events fired while the pointer is locked carry movementX/movementY deltas. This Core does not expose them, in any version up to this one. They are high-frequency data (potentially hundreds of events/sec) that do not fit the same-value-guarded, declarative properties model this protocol is built around — piping them through wc-bindable as-is would risk flooding the bound state with per-frame updates.
If a future version adds them, the design intent (see docs/pointer-lock-tag-design.md §3) is to gate them behind an explicit opt-in and pair them with @wcstack/debounce/@wcstack/throttle for rate-limiting, keeping the "no unbounded firehose" property intact for instances that don't opt in. For now, if you need raw movementX/movementY, read them directly off mousemove in your own imperative code alongside this node's active state.
Like @wcstack/fullscreen, this Shell does not lock itself — it is a hidden control tag that operates on a referenced element via the target attribute, using the same 3-mode resolution as @wcstack/intersection:
| target | operates on | display |
|---|---|---|
| omitted | first element child | contents |
| "#selector" | the matched element | none |
| "self" | the element itself | block |
npm install @wcstack/pointer-lock<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/pointer-lock/auto"></script>
<wcs-state>
<script type="module">
export default {
$commandTokens: ["lockPointer", "unlockPointer"],
locked: false,
};
</script>
</wcs-state>
<canvas id="scene" width="640" height="480"></canvas>
<wcs-pointer-lock target="#scene"
data-wcs="active: locked; command.requestPointerLock: $command.lockPointer; command.exitPointerLock: $command.unlockPointer">
</wcs-pointer-lock>
<button data-wcs="onclick: $command.lockPointer">Enable mouse look</button>
<button data-wcs="hidden: locked|not; onclick: $command.unlockPointer">Release</button>The buttons never touch <wcs-pointer-lock> directly: their clicks emit the lockPointer/unlockPointer command tokens, and <wcs-pointer-lock> subscribes to them via command.requestPointerLock: $command.lockPointer / command.exitPointerLock: $command.unlockPointer (the command-token protocol — the element with the command method is the subscriber, not the emitter).
Every bound state path must be declared up front — locked: false here; binding an undeclared path throws at initialization. Negation in a data-wcs path is done with the |not filter (locked|not), not a leading ! — paths do not support prefix operators.
requestPointerLock() requires a user-gesture context — see below.
Element.requestPointerLock() rejects with NotAllowedError when called outside a user-gesture context (e.g. a synchronous click handler). This node cannot manufacture a gesture on your behalf: the responsibility for calling requestPointerLock from within an actual user gesture belongs to the caller. Wire the command-token protocol (command.requestPointerLock: $command.<token> on <wcs-pointer-lock>, emitted by a button's onclick: $command.<token>) — calling it from a setTimeout or deep inside a .then() chain loses the gesture context and the call will reject, error will be set, but no exception will propagate (never-throw).
| Property | Event | Description |
|---|---|---|
| active | wcs-pointer-lock:change | true when document.pointerLockElement is this instance's resolved target, false otherwise. |
| error | wcs-pointer-lock:error | The most recent failure, or null. One of: a rejected promise (e.g. NotAllowedError for a gesture-less call), { message: "Pointer Lock API is not supported." } when the platform API is missing, { message: "Pointer Lock target could not be resolved." } when target did not resolve to an element, or null if the last attempt succeeded / nothing has failed yet. |
| errorInfo | wcs-pointer-lock:error-info-changed | Additive serializable failure taxonomy derived from error — { code, phase, recoverable, message } — or null. See below. |
error and errorInfo are both observable, event-backed wcBindable properties. (error was historically an imperative getter with no event of its own — that is no longer the case.) A wc-bindable binding core now delivers a request/exit failure reactively, so you can bind it with data-wcs / bind() rather than polling element.error after each command settles. The error value shape is unchanged; errorInfo is the additive serializable classification.
errorInfo maps each failure to a stable { code, phase, recoverable, message }:
| code | phase | recoverable | when |
|---|---|---|---|
| capability-missing | probe | false | Pointer Lock API is unsupported (neither standard nor legacy name present) |
| invalid-argument | start | false | target did not resolve to an element |
| not-allowed | execute | true | NotAllowedError / TypeError — called outside a user gesture; retrying inside a real gesture can succeed |
| pointer-lock-error | execute | false | any other caught exception |
The stable code values are exported as WCS_POINTER_LOCK_ERROR_CODE; the shared WcsIoErrorInfo / WcsIoErrorPhase types are re-exported from this package.
| Command | Async | Description |
|---|---|---|
| requestPointerLock | yes | Resolve target and call requestPointerLock() on it. Never-throw: failures (an unresolvable target, NotAllowedError for a missing gesture, or an unsupported API) are captured into error, not thrown. |
| exitPointerLock | no | Calls document.exitPointerLock(). Synchronous — unlike @wcstack/fullscreen's exitFullscreen() (which is Promise-based), exitPointerLock() returns void. Silent no-op if nothing is currently locked or the API is unsupported. |
| Attribute | Description |
|---|---|
| target | Selector (or "self") identifying the element to lock. See "The target attribute decides what is locked" above. Omitted → first element child. |
<wcs-pointer-lock> reflects its active output onto its ElementInternals CustomStateSet, so you can style it directly from CSS with the :state() pseudo-class — no data-wcs binding or extra class toggling required.
| State | On when |
|---|---|
| active | wcs-pointer-lock:change fires with true (cleared on false) |
wcs-pointer-lock:state(active) ~ .crosshair { display: block; }
wcs-pointer-lock:state(active) ~ .crosshair { display: none; } /* default */Unlike attributes or classes, :state() cannot be written from outside the element, so there is no risk of confusing this output state with an input. error / errorInfo are not reflected onto :state(): they are observable wcBindable properties (see "Observable Properties" above), but a failure object is not a boolean CSS state, so only active is wired to :state(). Observe error / errorInfo via data-wcs / bind() instead.
Browser support (:state(x) syntax): Chrome/Edge 125+, Safari 17.4+, Firefox 126+. In older browsers the states are simply never set — :state() selectors never match, but <wcs-pointer-lock> itself keeps working normally (graceful degradation, never-throw).
SSR: :state() cannot be serialized into HTML, so server-rendered markup never carries these states on first paint (@wcstack/server is unaffected). If you need to style the pre-hydration gap, pair your rule with wcs-pointer-lock:not(:defined) instead.
Custom states are invisible in DevTools' Elements panel and attachInternals() cannot be called twice, so there is no console way to inspect them directly. Two debug-only aids are provided for that:
el.debugStates — a snapshot array of the currently-on state names (e.g. ["active"]). It is not part of wc-bindable (not a bind target) and its shape is not a guaranteed contract — use it for debugging only.
The debug-states attribute (opt-in, default off) mirrors state changes onto a data-wcs-state-active attribute on the element, so the Elements panel highlights it as it toggles:
<wcs-pointer-lock target="#scene" debug-states></wcs-pointer-lock>Write your CSS against :state(), not data-wcs-state-*. The mirrored attribute exists purely to make state changes visible while debugging with DevTools open; it is not a supported styling hook.
document.pointerLockElement is a single document-wide value — at most one element can be locked at a time. When several <wcs-pointer-lock> instances exist simultaneously (e.g. target="#a" and target="#b"), each instance compares document.pointerLockElement against its own resolved target, not merely "is the document locked". Locking #a makes the target="#a" instance report active: true and the target="#b" instance report active: false — even though some element (#a) is locked document-wide.
Some older WebKit builds expose webkitRequestPointerLock / webkitExitPointerLock / webkitPointerLockElement / the webkitpointerlockchange event instead of the standard names. API resolution happens at call time (never cached), probing the standard name first and falling back to the legacy name — this lets an unsupported environment (neither name present) be detected correctly and lets tests install/remove the API freely.
The Core has no DOM dependency (beyond calling the Pointer Lock platform API) and can be used directly with bind() from @wc-bindable/core:
import { PointerLockCore } from "@wcstack/pointer-lock";
const lock = new PointerLockCore();
lock.addEventListener("wcs-pointer-lock:change", (e) => {
console.log((e as CustomEvent).detail); // boolean — the new `active` value directly
});
const canvas = document.querySelector("#scene")!;
lock.observe(canvas); // subscribe to document pointerlockchange, self-filtering on `canvas`
await lock.requestPointerLock(canvas); // must be called from within a user gesture
console.log(lock.active, lock.error);
// later, when done:
lock.exitPointerLock(); // synchronous
lock.dispose(); // detach the document listenerThe structural Core surface is normative across wcstack IO nodes (async-io-node-guidelines §3.9); to bind it into signals with no element at all, see @wcstack/signals — Binding a Core directly.
WCAG 2.1.2 No Keyboard Trap: the browser guarantees that Esc always releases the pointer lock — never repurpose or intercept that key. But a guaranteed escape hatch the user does not know about is only half an escape hatch: tell the user how to leave before the lock engages, and offer an in-app exit as well (a visible control wired to the exitPointerLock command). While locked, keep any instructions readable without pointer interaction.
MIT
| Back | FazBrowse Home | New Git URL |