Summary
On iOS Safari, world-space Spatial UI buttons receive forwarded UIKit/pointer events correctly, but actions that require browser user activation can fail when run from the forwarded pointerup/click handler.
The visible UI state updates, pointerenter, pointerdown, pointerup, and click handlers all fire on the UIKit element, but browser-gated APIs such as window.open() are not reliably treated as being inside the native Safari touch activation. Running the same action from a native DOM pointerup capture listener on the canvas/window succeeds.
This makes it look like forwardHtmlEvents is preserving the interaction semantically, but not preserving the native user-activation timing needed by iOS Safari.
Environment
@iwsdk/core: 0.3.2
- Three.js: r181 through IWSDK
@pmndrs/uikit: 1.0.64
- Browser: iOS Safari
- Mode: non-immersive browser mode, WebGL canvas, world-space
PanelUI
- Spatial UI config includes
features.spatialUI.forwardHtmlEvents: true
Minimal shape of the setup
World.create(container, {
xr: { /* normal WebXR config */ },
features: {
spatialUI: {
forwardHtmlEvents: true,
},
},
});
// A world-space PanelUI entity loads UIKit markup with a button-like element.
const button = document.getElementById('test-button');
button.addEventListener('pointerenter', () => {
// Visual hover/pressed affordance works on iOS Safari.
});
button.addEventListener('pointerup', () => {
window.open('https://example.com', '_blank', 'noopener,noreferrer');
});
button.addEventListener('click', () => {
window.open('https://example.com', '_blank', 'noopener,noreferrer');
});
Actual behavior
On iOS Safari:
- Touching the UIKit element visibly triggers the hover/press affordance.
- UIKit receives
pointerdown, pointerup, and click for the button.
- The handler runs.
window.open() does not reliably open a new tab/window.
- There may be no obvious app-level exception or console error from the handler.
A sanitized event trace from one tap looked like this:
ui pointerenter pointerType=screen-touch buttons=1 target=test-button
native pointerdown pointerType=touch buttons=1 target=canvas
native touchstart target=canvas
ui pointerdown pointerType=screen-touch buttons=1 target=test-button
native pointerup pointerType=touch buttons=0 target=canvas
native touchend target=canvas
ui pointerup pointerType=screen-touch buttons=0 target=test-button
ui click pointerType=screen-touch buttons=0 target=test-button
ui pointerleave pointerType=screen-touch buttons=0 target=test-button
native click pointerType=mouse buttons=0 target=canvas
The important part is that the native DOM pointerup occurs before the forwarded UIKit pointerup/click. Browser user activation appears to still be valid in the native DOM event, but not by the time the forwarded UIKit event handler runs.
Expected behavior
A tap on a forwarded Spatial UI button should allow common user-activation-dependent actions to run from the button handler on iOS Safari, especially:
window.open(...)
- entering or launching an XR/session flow where the browser requires a user gesture
- similar browser-gated APIs that need to run during native activation
Workaround that succeeds
A local workaround is to bridge native touch events manually:
- Track the currently hovered UIKit button from forwarded
pointerenter where pointerType includes screen-touch.
- Add native
window capture listeners for DOM pointerdown/pointerup where event.pointerType === 'touch'.
- On native DOM
pointerup, if the pointer is still within tap movement/time thresholds, run the mapped button action immediately.
- Suppress the later forwarded UIKit
pointerup/click duplicate.
That makes window.open() work on iOS Safari, which strongly suggests this is an activation-timing issue rather than hit testing or event delivery.
Possible cause
Looking at the event flow, this may be related to forwarded HTML events being batched and emitted later from the Spatial UI update pass rather than synchronously during the native DOM pointer event. iOS Safari is strict about user activation being consumed in the native event task.
A possible SDK-level fix might be one of:
- Dispatch forwarded HTML pointer/click events synchronously for activation-sensitive events.
- Expose a
batchEvents: false option through IWSDK's spatialUI.forwardHtmlEvents configuration.
- Provide a first-class Spatial UI activation event/hook that runs during the native DOM touch event while still resolving to the hit UIKit element.
Why this matters
The current behavior is confusing because the button visibly reacts and the UIKit handler fires, so app code appears correct. The failure only shows up when the handler uses a browser-gated API on iOS Safari.
Summary
On iOS Safari, world-space Spatial UI buttons receive forwarded UIKit/pointer events correctly, but actions that require browser user activation can fail when run from the forwarded
pointerup/clickhandler.The visible UI state updates,
pointerenter,pointerdown,pointerup, andclickhandlers all fire on the UIKit element, but browser-gated APIs such aswindow.open()are not reliably treated as being inside the native Safari touch activation. Running the same action from a native DOMpointerupcapture listener on the canvas/window succeeds.This makes it look like
forwardHtmlEventsis preserving the interaction semantically, but not preserving the native user-activation timing needed by iOS Safari.Environment
@iwsdk/core: 0.3.2@pmndrs/uikit: 1.0.64PanelUIfeatures.spatialUI.forwardHtmlEvents: trueMinimal shape of the setup
Actual behavior
On iOS Safari:
pointerdown,pointerup, andclickfor the button.window.open()does not reliably open a new tab/window.A sanitized event trace from one tap looked like this:
The important part is that the native DOM
pointerupoccurs before the forwarded UIKitpointerup/click. Browser user activation appears to still be valid in the native DOM event, but not by the time the forwarded UIKit event handler runs.Expected behavior
A tap on a forwarded Spatial UI button should allow common user-activation-dependent actions to run from the button handler on iOS Safari, especially:
window.open(...)Workaround that succeeds
A local workaround is to bridge native touch events manually:
pointerenterwherepointerTypeincludesscreen-touch.windowcapture listeners for DOMpointerdown/pointerupwhereevent.pointerType === 'touch'.pointerup, if the pointer is still within tap movement/time thresholds, run the mapped button action immediately.pointerup/clickduplicate.That makes
window.open()work on iOS Safari, which strongly suggests this is an activation-timing issue rather than hit testing or event delivery.Possible cause
Looking at the event flow, this may be related to forwarded HTML events being batched and emitted later from the Spatial UI update pass rather than synchronously during the native DOM pointer event. iOS Safari is strict about user activation being consumed in the native event task.
A possible SDK-level fix might be one of:
batchEvents: falseoption through IWSDK'sspatialUI.forwardHtmlEventsconfiguration.Why this matters
The current behavior is confusing because the button visibly reacts and the UIKit handler fires, so app code appears correct. The failure only shows up when the handler uses a browser-gated API on iOS Safari.