Bringing UI to Life on Mobile with Sensor APIs
Alexander Stepanchenko
Original version: https://habr.com/ru/articles/1010328/
This is about making web interfaces react to device tilt — shifting highlights, shadows, and subtle lighting — to add interactivity and a sense of depth.
Device Orientation APIs have been around for a long time. Mobile devices with gyroscopes have effectively become the main interface for apps and websites. At the same time, emotional UI design is trending: glossy effects, gradients, and “shine” interactions are everywhere. It feels like the right moment to bring all of this together.
Apple recently introduced something similar with Liquid Glass. But honestly, in my opinion, it didn’t fully land — or it got overshadowed by other announcements. Which is a pity, because I believe UI reacting to device movement is far more interesting than just another transparency effect with fancy refraction physics that everyone rushes to copy.
Unlike background refraction tricks, reacting to device tilt is not just visual decoration. It’s a micro-interaction that creates a sense of control, responsiveness, and “alive” interfaces.
Even when users aren’t tapping the screen, they’re still interacting — reading, looking, slightly moving the phone in their hands. The UI can respond to those micro-movements with subtle shifts in light, shadow, and depth, as if elements are not drawn, but physically exist.
Of course, this is subjective. Some people get annoyed or even “motion sick” from extra animations. Thankfully, there’s prefers-reduced-motion. There is just no equivalent switch for “make it look nicer”.
If it’s still unclear what I mean, check this video:
Pay attention to:
- highlights on card ed
- ges
- subtle moving gradients in checkboxes
- light reflections on icons
You can also try this effect on a mobile device inside a Telegram Mini App (dark mode works best — it makes the effect more visible). There’s also a standalone demo.
Sensors
Modern devices are packed with MEMS sensors (micro-electro-mechanical systems — a topic worth exploring on its own, especially how accelerometers work internally).
For example, modern iPhones include:
- Barometer (measures atmospheric pressure for altitude estimation)
- High dynamic range gyroscope (3-axis orientation tracking, image stabilization)
- High-g accelerometer (detects acceleration, rotation, steps)
- Proximity sensor (turns off screen near face during calls)
- Dual ambient light sensors (adjust screen brightness)
- Magnetometer (compass for navigation)
Some Android devices may also include additional sensors like temperature sensors or Hall effect sensors.
For this article, we focus only on spatial orientation sensors:
- Gyroscope
- Accelerometer
- Magnetometer (intentionally ignored)
The key thing for frontend developers: these sensors are extremely sensitive, noisy, and can drain battery if misused. They can also overwhelm the main thread with events if not handled properly.
So if you don’t want your UI to jitter, lag, or “shake”, you need to handle that properly. Fortunately, it’s manageable.
We avoid magnetometer here due to noise and interference. Modern APIs can already estimate orientation using a combination of gyroscope + accelerometer.
APIs
Around 2010–2011, DeviceOrientation and DeviceMotion appeared in Mobile Safari and Android Browser, later adopted by Firefox.
Back then, libraries like gyro.js emerged — I remember building a very average perspective effect for my personal site using it.
The existence of such libraries already hints at limitations in the API:
- no control over update frequency
- unstable values
- combined sensor streams (gyro + accelerometer together)
- limited flexibility (especially in Safari today)
Basic usage looked like this:
window.addEventListener('deviceorientation', throttle(function(move){
console.log(move.alpha, move.beta, move.gamma)
}, 111))
Generic Sensor API
Around 2016–2018, W3C introduced the Generic Sensor API:
const sensor = new RelativeOrientationSensor({ frequency: 60 })
sensor.addEventListener("reading", () => {
const [x, y, z, w] = sensor.quaternion
})
sensor.start()
However, Apple never fully adopted it due to privacy and fingerprinting concerns, sticking with DeviceOrientation.
On top of that, in iOS 13 (2019), Apple introduced explicit permission:
DeviceOrientationEvent.requestPermission()
And it’s session-based — meaning users may need to re-approve it repeatedly.
This likely slowed down adoption of sensor-based web UI effects significantly.
Telegram Mini Apps
When Telegram introduced native sensor access inside Mini Apps, it became a game changer:
window.Telegram?.WebApp.DeviceOrientation.start({ refresh_rate: 42 })
This works on iOS without permission prompts, giving access to accelerometer, gyroscope, and orientation data.
This is a big unlock for interactive UI effects.
Native Apps / WebView
In native apps or WebViews, you can also use platform APIs:
iOS (CoreMotion)
Android (SensorManager)
Then pass values into WebView:
let motionManager = CMMotionManager()
motionManager.deviceMotionUpdateInterval = 1.0 / 30.0
motionManager.startDeviceMotionUpdates(to: .main) { motion, error in
guard let motion = motion else { return }
let q = motion.attitude.quaternion
let payload: [String: Any] = [
"type": "relative-orientation",
"quaternion": [q.x, q.y, q.z, q.w],
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
"source": "native-ios"
]
let json = try! JSONSerialization.data(withJSONObject: payload)
let jsonString = String(data: json, encoding: .utf8)!
let js = "window.__onMotion(\(jsonString));"
webView.evaluateJavaScript(js)
}
Battery Considerations
Sensor usage does increase battery consumption, but mostly due to JavaScript processing overhead, not the sensors themselves.
Gyroscopes and accelerometers are actually quite efficient (1–3 mA·h range).
For comparison:
- screen brightness: ~20–50 mA·h
- GPS: ~30–60 mA·h
- Wi-Fi (active): ~10–200 mA·h
So the real optimization concern is update frequency and JS work.
A simple safeguard:
if ((await navigator.getBattery?.())?.level < 0.5) return
Debugging
Important requirement: HTTPS is mandatory. Localhost won’t provide sensor data.
Use tools like ngrok or localtunnel for local testing.
Chrome DevTools allows sensor emulation:
Cmd+Shift+P → “Sensors”
Safari lacks this, so real-device testing is essential. Only on a real phone can you notice jitter, lag, and over-smoothing issues.
Implementation
At a high level:
We take two axes corresponding to horizontal and vertical tilt, normalize them to 0–100%, and expose them as CSS variables.
transform: translateY(calc(60% - var(--gyro-gamma-percent) * 2%));
Basic DeviceOrientation
window.addEventListener('deviceorientation', e => {
document.documentElement.style.setProperty('--gyro-gamma', e.gamma);
document.documentElement.style.setProperty('--gyro-beta', e.beta);
});
:root {
--gyro-gamma: 50;
--gyro-beta: 50;
}
A simple demo: a dot moves based on tilt.
Safari Permission
document.querySelector('#test').addEventListener('click', async () => {
if (typeof DeviceOrientationEvent?.requestPermission !== 'function') return;
const result = await DeviceOrientationEvent.requestPermission();
console.log(result);
});
Throttling
function throttle(fn, ms) {
let last = 0;
return (...args) => {
const now = performance.now();
if (now - last < ms) return;
last = now;
fn(...args);
};
}
Normalization (0–100%)
Raw angles are not convenient, so we normalize them:
- 50 = neutral
- 0 / 100 = extremes
This makes CSS much simpler.
const GAMMA_RANGE = 70;
const BETA_OFFSET = 45;
const BETA_RANGE = 45;
function clamp(v, min = -1, max = 1) {
return Math.min(max, Math.max(min, v));
}
RelativeOrientationSensor
Modern API alternative with fixed frequency and quaternion output:
const sensor = new RelativeOrientationSensor({
frequency: 42,
referenceFrame: "screen"
});
sensor.addEventListener('reading', () => {
const [qx, qy, qz, qw] = sensor.quaternion;
});
This gives quaternion data — a 4D representation of rotation used in physics and 3D graphics.
We convert it back into gamma/beta for consistency with older APIs.
Telegram API Shortcut
function initTWASensor() {
const TWA = window.Telegram?.WebApp;
if (!TWA?.DeviceOrientation) return false;
TWA.onEvent('deviceOrientationChanged', () => {
const { gamma, beta } = TWA.DeviceOrientation;
document.documentElement.style.setProperty("--gyro-gamma", clamp(gamma));
document.documentElement.style.setProperty("--gyro-beta", clamp(beta));
});
}
Reduced Motion
Always respect accessibility settings:
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
return;
}
CSS Effects
Once we have:
- --gyro-gamma
- --gyro-beta
We can build effects like:
Highlights
Moving light reflections across cards.
Icon shine
Subtle glows that shift with tilt.
Gradient movement
Background gradients that react to orientation.
Checkbox depth
Old Apple-style checkboxes with moving light layers.
Conclusion
Full code and examples are available here:
https://github.com/alexstep/sensor/
This is not a production-ready library — just an experimental implementation built for this article. It will definitely need adaptation for real projects.