- CSS-driven — No
requestAnimationFrameloop. CSS custom properties + compositor = GPU-accelerated. - ~2KB gzipped — Core engine only.
- Accelerometer — Auto-detects gyroscope, handles iOS permissions transparently.
- Multi-layer parallax —
data-levita-offseton children for depth layering. - Framework first — Official, lightweight wrappers for React, Vue, Svelte & Angular. See examples.
- Styling agnostic — Works with any CSS framework (Tailwind, Bootstrap, etc.) or vanilla CSS. See examples.
Ecosystem Effects Install Quick Start Parallax Layers Grouped Instances Accelerometer Options Events Methods How It Works Comparison Benchmarks Contributing Roadmap License
Levita is designed to be framework-agnostic. Choose your flavor:
| Framework | Min Version | Size (gzip) | Playground |
|---|---|---|---|
| - | |||
| Tilt | Glare |
|---|---|
![]() |
![]() |
| Shadow | Combined |
![]() |
![]() |
# Core (vanilla JS/TS)
npm install levita-js
# React wrapper
npm install @levita-js/react
# Vue wrapper
npm install @levita-js/vue
# Svelte wrapper
npm install @levita-js/svelte
# Angular wrapper
npm install @levita-js/angularimport { Levita } from "levita-js";
import "levita-js/style.css";
new Levita(document.getElementById("card"), {
glare: true,
shadow: true,
});import { Tilt } from "@levita-js/react";
import "@levita-js/react/style.css";
function Card() {
return (
<Tilt glare shadow>
<h2>Hello</h2>
</Tilt>
);
}<script setup>
import { Tilt } from "@levita-js/vue";
import "@levita-js/vue/style.css";
</script>
<template>
<Tilt glare shadow>
<h2>Hello</h2>
</Tilt>
</template><script>
import { tilt } from "@levita-js/svelte";
import "@levita-js/svelte/style.css";
</script>
<div use:tilt={{ glare: true, shadow: true }}>
<h2>Hello</h2>
</div>import { LevitaDirective } from "@levita-js/angular";
import "@levita-js/angular/style.css";
@Component({
selector: "app-card",
standalone: true,
imports: [LevitaDirective],
template: `
<div [levita]="{ glare: true, shadow: true }">
<h2>Hello</h2>
</div>
`,
})
export class CardComponent {}Add data-levita-offset to children for multi-depth parallax. Positive values come forward, negative go back:
<div id="scene">
<img data-levita-offset="-5" src="bg.png" />
<img data-levita-offset="0" src="mid.png" />
<img data-levita-offset="10" src="fg.png" />
</div>new Levita(document.getElementById("scene"));You can make multiple Levita instances react to the same pointer movement by using the eventsEl option. This is useful for grids where all cards should tilt together:
const container = document.getElementById("grid-container");
const cards = document.querySelectorAll(".card");
for (const card of cards) {
new Levita(card, { eventsEl: container });
}Levita auto-detects accelerometer support:
- Android — works immediately, no permission needed.
- iOS 13+ — permission requested on first touch (silent fallback if denied).
// Auto mode (default) — handles everything
new Levita(el, { gyroscope: "auto" });
// Manual mode — you control when to ask
const instance = new Levita(el, { gyroscope: true });
button.addEventListener("click", async () => {
const granted = await instance.requestPermission();
console.log("Gyroscope:", granted ? "enabled" : "denied");
});
// Disabled
new Levita(el, { gyroscope: false });new Levita(el, {
gyroscope: "auto",
gyroRange: 40, // More reactive (less tilt needed)
gyroSmoothing: 0.1, // Smoother movement
});gyroRange— Total physical tilt range in degrees mapped to full effect. Lower = more reactive. Default:60.gyroSmoothing— Exponential moving average factor. Lower = smoother but more latent. Default:0.15.
| Option | Type | Default | Description |
|---|---|---|---|
max |
number |
15 |
Max tilt angle in degrees |
perspective |
number |
1000 |
CSS perspective in px |
scale |
number |
1.05 |
Scale factor on hover |
speed |
number |
200 |
Transition duration in ms |
easing |
string |
'ease-out' |
CSS easing function |
reverse |
boolean |
false |
Invert tilt direction |
axis |
'x' | 'y' | null |
null |
Lock to single axis |
reset |
boolean |
true |
Reset on pointer leave |
glare |
boolean |
false |
Enable glare effect |
maxGlare |
number |
0.5 |
Max glare opacity (0-1) |
shadow |
boolean |
false |
Enable dynamic shadow |
gyroscope |
'auto' | boolean |
'auto' |
Accelerometer mode |
gyroRange |
number |
60 |
Physical tilt range in degrees |
gyroSmoothing |
number |
0.15 |
Sensor smoothing factor (0-1) |
disabled |
boolean |
false |
Disable the effect |
eventsEl |
HTMLElement | null |
null |
Element to listen for events on |
const instance = new Levita(el);
instance.on("move", ({ x, y, percentX, percentY }) => {
console.log(`Tilt: ${x}°, ${y}°`);
});
instance.on("enter", () => console.log("Pointer entered"));
instance.on("leave", () => console.log("Pointer left"));
// Remove listener
instance.off("move", handler);instance.enable(); // Re-enable after disable
instance.disable(); // Pause and reset
instance.destroy(); // Full cleanup
await instance.requestPermission(); // Manual gyroscope permissionMost tilt libraries (like vanilla-tilt) run a requestAnimationFrame loop that recalculates and applies the transform matrix on every frame in JavaScript. This means JS is active between every frame, and high-polling-rate mice (1000Hz+) can flood the main thread with style recalculations.
Levita takes a different approach with CSS custom properties:
- Pointer or accelerometer input fires → JS computes the tilt angle
- Updates are coalesced via
requestAnimationFrame— even if the mouse fires 1000 events/sec, only one DOM update happens per frame - JS sets
--levita-xand--levita-yas CSS custom properties on the element - A CSS
transformrule reads those properties, andtransitionsmooths the movement — entirely on the GPU compositor thread
pointer/gyro event (may fire at 1000Hz)
│
▼
rAF coalescing (1 update per frame)
│
▼
JS sets --levita-x, --levita-y ← only JS work
│
▼
CSS transform + transition ← GPU compositor, no JS
The result:
- No JS between frames — JavaScript only runs when input changes, once per frame
- GPU-accelerated — the browser's compositor thread handles the animation
- High-polling-rate safe — 1000Hz mice don't saturate the main thread
- Lower CPU usage — measured via Vitest bench, see Benchmarks
| Feature | Levita | Atropos | vanilla-tilt |
|---|---|---|---|
| Bundle size (gzip) | ~2KB | ~2KB | ~3-4KB |
| Animation strategy | CSS custom properties | CSS transitions | rAF loop |
| Tree-shakeable | ✅ | ❌ | ❌ |
| Multi-layer parallax | ✅ (data attrs) | ✅ (data attrs) | ❌ |
| Accelerometer | Auto + manual (calibrated) | ❌ | Partial (no calibration) |
| Grouped instances | ✅ (eventsEl) |
✅ (stretchX/Y/Z) |
✅ (mouse-event-element) |
| Runtime option update | ✅ (update()) |
❌ | ❌ |
| React | Official wrapper | Official wrapper | Community |
| Vue | Official wrapper | Web Component | Community |
| Svelte | Official wrapper | ❌ | ❌ |
| Angular | Official wrapper | Web Component | Community |
| TypeScript | Native (source in TS) | Declaration file | DefinitelyTyped |
| Last published | 2026 | 2023 | 2021 |
Measured with Vitest bench (happy-dom):
| Scenario | ops/s |
|---|---|
| Basic init + destroy | |
| Init with glare + shadow | |
| Init with 5 parallax layers | |
| Pointer move update | |
| Pointer move with glare + shadow |
Run locally: pnpm bench
Automatically updated at each release.
Levita is a monorepo managed with pnpm. For details on how to set up the development environment, run tests, and understand our release workflow, check out our Development Guide.
If you find Levita useful, consider supporting its development:
Interested in what's coming next? You can follow the development progress and upcoming features on our public project board:
MIT



