Tilt (gyroscope) parallax
On a phone the layers move as you tilt the device; on desktop the same scene follows the mouse. iPhone asks permission first.
CSSJavaScriptAdvanced
<div class="scene">
<div class="l stars" data-depth="0.2"></div>
<div class="l moon" data-depth="0.5"></div>
<div class="l hill h1" data-depth="1"></div>
<div class="l hill h2" data-depth="1.8"></div>
<div class="l title" data-depth="0.3"><div><h1>Tilt me</h1><p id="mode">Move the mouse, or tilt your phone</p></div></div>
</div>
<button id="ask">Enable motion</button>
body{overflow:hidden;background:linear-gradient(#140807,#2a0f0b)}
.scene{position:fixed;inset:0}
.l{position:absolute;will-change:transform}
.moon{left:58%;top:16%;width:90px;height:90px;border-radius:50%;background:#fff4c9;box-shadow:0 0 50px rgba(255,244,201,.5)}
.hill{left:-10%;right:-10%;border-radius:50% 50% 0 0}
.h1{bottom:-30%;height:60%;background:#4e1408}
.h2{bottom:-38%;height:60%;background:#2a0f0b}
.stars{inset:-5%;background-image:radial-gradient(2px 2px at 20% 30%,#fff,transparent),radial-gradient(2px 2px at 70% 20%,#fff,transparent),radial-gradient(1.5px 1.5px at 40% 60%,#fff,transparent),radial-gradient(1.5px 1.5px at 85% 55%,#fff,transparent),radial-gradient(2px 2px at 12% 75%,#fff,transparent)}
.title{inset:0;display:grid;place-items:center;text-align:center;pointer-events:none}
.title h1{margin:0;font-size:clamp(2rem,9vw,3.6rem);letter-spacing:-.03em}
.title p{margin:6px 0 0;color:#cfc3c1;font-size:14px}
#ask{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);z-index:5;font:inherit;font-weight:700;background:#f08a5d;color:#270100;border:0;border-radius:10px;padding:10px 16px;cursor:pointer;display:none}
const layers = [...document.querySelectorAll('[data-depth]')];
let tx = 0, ty = 0, cx = 0, cy = 0;
const clamp = (v, a, b) => Math.min(Math.max(v, a), b);
// Desktop fallback: mouse
addEventListener('pointermove', e => {
tx = (e.clientX / innerWidth - .5) * 2;
ty = (e.clientY / innerHeight - .5) * 2;
});
// Phone: device orientation
function listen() {
addEventListener('deviceorientation', e => {
if (e.gamma == null) return;
tx = clamp(e.gamma / 25, -1, 1); // left / right tilt
ty = clamp(((e.beta || 0) - 45) / 25, -1, 1); // forward / back tilt (45deg = holding it naturally)
document.getElementById('mode').textContent = 'Using your motion sensor';
});
}
const ask = document.getElementById('ask');
if (window.DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === 'function') {
ask.style.display = 'block'; // iPhone: must be asked from a tap
ask.onclick = async () => {
if ((await DeviceOrientationEvent.requestPermission()) === 'granted') { listen(); ask.remove(); }
};
} else {
listen(); // Android and others
}
(function loop() {
cx += (tx - cx) * 0.1;
cy += (ty - cy) * 0.1;
layers.forEach(l => {
const d = l.dataset.depth * 30;
l.style.transform = `translate3d(${-cx * d}px, ${-cy * d}px, 0)`;
});
requestAnimationFrame(loop);
})();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tilt parallax</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
body{overflow:hidden;background:linear-gradient(#140807,#2a0f0b)}
.scene{position:fixed;inset:0}
.l{position:absolute;will-change:transform}
.moon{left:58%;top:16%;width:90px;height:90px;border-radius:50%;background:#fff4c9;box-shadow:0 0 50px rgba(255,244,201,.5)}
.hill{left:-10%;right:-10%;border-radius:50% 50% 0 0}
.h1{bottom:-30%;height:60%;background:#4e1408}
.h2{bottom:-38%;height:60%;background:#2a0f0b}
.stars{inset:-5%;background-image:radial-gradient(2px 2px at 20% 30%,#fff,transparent),radial-gradient(2px 2px at 70% 20%,#fff,transparent),radial-gradient(1.5px 1.5px at 40% 60%,#fff,transparent),radial-gradient(1.5px 1.5px at 85% 55%,#fff,transparent),radial-gradient(2px 2px at 12% 75%,#fff,transparent)}
.title{inset:0;display:grid;place-items:center;text-align:center;pointer-events:none}
.title h1{margin:0;font-size:clamp(2rem,9vw,3.6rem);letter-spacing:-.03em}
.title p{margin:6px 0 0;color:#cfc3c1;font-size:14px}
#ask{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);z-index:5;font:inherit;font-weight:700;background:#f08a5d;color:#270100;border:0;border-radius:10px;padding:10px 16px;cursor:pointer;display:none}
</style></head>
<body>
<div class="scene">
<div class="l stars" data-depth="0.2"></div>
<div class="l moon" data-depth="0.5"></div>
<div class="l hill h1" data-depth="1"></div>
<div class="l hill h2" data-depth="1.8"></div>
<div class="l title" data-depth="0.3"><div><h1>Tilt me</h1><p id="mode">Move the mouse, or tilt your phone</p></div></div>
</div>
<button id="ask">Enable motion</button>
<script>
const layers = [...document.querySelectorAll('[data-depth]')];
let tx = 0, ty = 0, cx = 0, cy = 0;
const clamp = (v, a, b) => Math.min(Math.max(v, a), b);
// Desktop fallback: mouse
addEventListener('pointermove', e => {
tx = (e.clientX / innerWidth - .5) * 2;
ty = (e.clientY / innerHeight - .5) * 2;
});
// Phone: device orientation
function listen() {
addEventListener('deviceorientation', e => {
if (e.gamma == null) return;
tx = clamp(e.gamma / 25, -1, 1); // left / right tilt
ty = clamp(((e.beta || 0) - 45) / 25, -1, 1); // forward / back tilt (45deg = holding it naturally)
document.getElementById('mode').textContent = 'Using your motion sensor';
});
}
const ask = document.getElementById('ask');
if (window.DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === 'function') {
ask.style.display = 'block'; // iPhone: must be asked from a tap
ask.onclick = async () => {
if ((await DeviceOrientationEvent.requestPermission()) === 'granted') { listen(); ask.remove(); }
};
} else {
listen(); // Android and others
}
(function loop() {
cx += (tx - cx) * 0.1;
cy += (ty - cy) * 0.1;
layers.forEach(l => {
const d = l.dataset.depth * 30;
l.style.transform = `translate3d(${-cx * d}px, ${-cy * d}px, 0)`;
});
requestAnimationFrame(loop);
})();
</script>
</body></html>
Good for
Mobile hero scenes, cards and game-like landing pages.
Watch out for
iOS needs DeviceOrientationEvent.requestPermission() from a tap, and the page must be HTTPS. Always offer the mouse/touch fallback.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.