diff --git a/src/components/look-on-mobile.js b/src/components/look-on-mobile.js
new file mode 100644
index 0000000000000000000000000000000000000000..004d7a625e521a4f6217a76b9f78d7d83ba20134
--- /dev/null
+++ b/src/components/look-on-mobile.js
@@ -0,0 +1,63 @@
+const PolyfillControls = AFRAME.utils.device.PolyfillControls;
+const PI_4 = Math.PI / 4;
+
+AFRAME.registerComponent("look-on-mobile", {
+  schema: {
+    enabled: { default: false },
+    horizontalLookSpeedRatio: { default: 0.4 }, // motion of polyfill object / motion of camera
+    verticalLookSpeedRatio: { default: 0.4 } // motion of polyfill object / motion of camera
+  },
+
+  init() {
+    this.hmdEuler = new THREE.Euler();
+    this.prevX = this.hmdEuler.x;
+    this.prevY = this.hmdEuler.y;
+    this.polyfillObject = new THREE.Object3D();
+    this.polyfillControls = new PolyfillControls(this.polyfillObject);
+    this.ticks = 0;
+    this.pendingLookX = 0;
+    this.onRotateX = this.onRotateX.bind(this);
+  },
+
+  play() {
+    this.el.addEventListener("rotateX", this.onRotateX);
+  },
+  pause() {
+    this.el.removeEventListener("rotateX", this.onRotateX);
+  },
+  onRotateX(e) {
+    this.pendingLookX = e.detail.value * 2;
+  },
+
+  registerLookControls(lookControls) {
+    this.lookControls = lookControls;
+    this.lookControls.data.enabled = false;
+    this.lookControls.polyfillControls.update = () => {};
+  },
+
+  tick(t, dt) {
+    if (!this.data.enabled) return;
+    const scene = this.el.sceneEl;
+    const rotation = this.rotation;
+    const hmdEuler = this.hmdEuler;
+    const pitchObject = this.lookControls.pitchObject;
+    const yawObject = this.lookControls.yawObject;
+    const { horizontalLookSpeedRatio, verticalLookSpeedRatio } = this.data;
+    if (scene.is("vr-mode") && scene.checkHeadsetConnected()) return;
+    this.polyfillControls.update();
+    hmdEuler.setFromQuaternion(this.polyfillObject.quaternion, "YXZ");
+
+    const joystick = this.pendingLookX * dt / 1000;
+    const dX = joystick + hmdEuler.x - this.prevX;
+    const dY = hmdEuler.y - this.prevY;
+
+    yawObject.rotation.y += dY * horizontalLookSpeedRatio;
+    pitchObject.rotation.x += dX * verticalLookSpeedRatio;
+    pitchObject.rotation.x = Math.max(-PI_4, Math.min(PI_4, pitchObject.rotation.x));
+
+    this.lookControls.updateOrientation();
+    this.prevX = hmdEuler.x;
+    this.prevY = hmdEuler.y;
+    this.pendingLookX = 0;
+  }
+});
diff --git a/src/hub.html b/src/hub.html
index 861ef092edbddebca3e4ce9335ebed7c478f91a6..531b838713b4d6810634f0e5266de24fa3b06e26 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -38,6 +38,7 @@
         personal-space-bubble="debug: false;"
         vr-mode-ui="enabled: false"
         pinch-to-move
+        look-on-mobile
     >
 
         <a-assets>
diff --git a/src/hub.js b/src/hub.js
index 5adf975702ed139b6ec5a2095c2373ae413d0e56..d38367bded72dab008cdbc32caee8db706681f0f 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -64,6 +64,7 @@ import "./components/css-class";
 import "./components/scene-shadow";
 import "./components/avatar-replay";
 import "./components/pinch-to-move";
+import "./components/look-on-mobile";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -242,10 +243,15 @@ const onReady = async () => {
     const registerLookControls = e => {
       if (e.detail.name !== "look-controls") return;
       window.APP.touchEventsHandler.registerLookControls(camera.components["look-controls"]);
-      camera.removeEventListener("commponentinitialized", registerLookControls);
+      camera.removeEventListener("componentinitialized", registerLookControls);
+      scene.components["look-on-mobile"].registerLookControls(camera.components["look-controls"]);
+      scene.setAttribute("look-on-mobile", "enabled", true);
     };
     camera.addEventListener("componentinitialized", registerLookControls);
-    camera.setAttribute("look-controls", "touchEnabled", false);
+    camera.setAttribute("look-controls", {
+      touchEnabled: false,
+      hmdEnabled: false
+    });
 
     scene.setAttribute("networked-scene", {
       room: hubId,
diff --git a/src/utils/touch-events-handler.js b/src/utils/touch-events-handler.js
index 5532db5830f90f6cfe2b43cd7432e73814b10192..423a40aeaa373b9408d966a8cedaea542ef83cdd 100644
--- a/src/utils/touch-events-handler.js
+++ b/src/utils/touch-events-handler.js
@@ -70,7 +70,6 @@ export default class TouchEventsHandler {
     if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) return;
     if (!this.touchReservedForCursor && this.cursor.handleTouchStart(touch)) {
       this.touchReservedForCursor = touch;
-      return;
     }
     this.touches.push(touch);
   }
@@ -146,16 +145,16 @@ export default class TouchEventsHandler {
   }
 
   singleTouchEnd(touch) {
-    const touchIndex = this.touches.findIndex(t => touch.identifier === t.identifier);
-    if (touchIndex === -1) return;
-    this.touches.splice(touchIndex, 1);
-
     if (this.touchReservedForCursor && touch.identifier === this.touchReservedForCursor.identifier) {
       this.cursor.handleTouchEnd(touch);
       this.touchReservedForCursor = null;
       return;
     }
 
+    const touchIndex = this.touches.findIndex(t => touch.identifier === t.identifier);
+    if (touchIndex === -1) return;
+    this.touches.splice(touchIndex, 1);
+
     const pinchIndex = this.touchesReservedForPinch.findIndex(t => touch.identifier === t.identifier);
     if (pinchIndex !== -1) {
       this.touchesReservedForPinch.splice(pinchIndex, 1);