README.md

May 16, 2026 · View on GitHub

Levita — Lightweight 3D tilt & parallax library with accelerometer support

CI npm npm downloads bundle size StackBlitz license demo

  • CSS-driven — No requestAnimationFrame loop. CSS custom properties + compositor = GPU-accelerated.
  • ~2KB gzipped — Core engine only.
  • Accelerometer — Auto-detects gyroscope, handles iOS permissions transparently.
  • Multi-layer parallaxdata-levita-offset on 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.

Table of Contents

Ecosystem

Levita is designed to be framework-agnostic. Choose your flavor:

FrameworkMin VersionSize (gzip)Playground
Vanilla JS Vanilla JS-core sizeTry Vanilla JS on StackBlitz
React Reactreact min versionreact sizeTry React on StackBlitz
Vue Vuevue min versionvue sizeTry Vue on StackBlitz
Svelte Sveltesvelte min versionsvelte sizeTry Svelte on StackBlitz
Angular Angularangular min versionangular sizeTry Angular on StackBlitz

Effects

TiltGlare
TiltGlare
ShadowCombined
ShadowCombined

Install

# 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/angular

Quick Start

Vanilla

import { Levita } from "levita-js";
import "levita-js/style.css";

new Levita(document.getElementById("card"), {
  glare: true,
  shadow: true,
});

Open Vanilla JS example in StackBlitz

React

import { Tilt } from "@levita-js/react";
import "@levita-js/react/style.css";

function Card() {
  return (
    <Tilt glare shadow>
      <h2>Hello</h2>
    </Tilt>
  );
}

Open React example in StackBlitz

Vue

<script setup>
import { Tilt } from "@levita-js/vue";
import "@levita-js/vue/style.css";
</script>

<template>
  <Tilt glare shadow>
    <h2>Hello</h2>
  </Tilt>
</template>

Open Vue example in StackBlitz

Svelte

<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>

Open Svelte example in StackBlitz

Angular

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 {}

Open Angular example in StackBlitz

Parallax Layers

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"));

Active Offset (Discovery Effect)

The "Active Offset" effect creates a "window" illusion where the background image moves in the opposite direction of the tilt, revealing hidden edges.

  1. Enable it via the activeOffset option (in pixels).
  2. Add data-levita-active to the background element.
  3. Automatic Scaling: Levita automatically calculates and applies the minimum required scale() to the element so that hidden edges are revealed during tilt without showing gaps.
<div id="card" style="overflow: hidden; border-radius: 12px;">
  <!-- Levita handles the zoom and movement automatically -->
  <img
    data-levita-active
    src="bg.jpg"
    style="width: 100%; height: 100%; object-fit: cover;"
  />
  <h2 data-levita-offset="20">Floating Text</h2>
</div>
new Levita(document.getElementById("card"), {
  activeOffset: 20, // Move bg up to 20px to reveal edges
});

Grouped Instances

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 });
}

Accelerometer

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 });

Fine-tuning gyroscope behavior

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.

Options

OptionTypeDefaultDescription
maxnumber15Max tilt angle in degrees
perspectivenumber1000CSS perspective in px
scalenumber1.05Scale factor on hover
speednumber200Transition duration in ms
easingstring'ease-out'CSS easing function
reversebooleanfalseInvert tilt direction
axis'x' | 'y' | nullnullLock to single axis
resetbooleantrueReset on pointer leave
glarebooleanfalseEnable glare effect
maxGlarenumber0.5Max glare opacity (0-1)
shadowbooleanfalseEnable dynamic shadow
gyroscope'auto' | boolean'auto'Accelerometer mode
gyroRangenumber60Physical tilt range in degrees
gyroSmoothingnumber0.15Sensor smoothing factor (0-1)
activeOffsetnumber0Active parallax offset for backgrounds in px
disabledbooleanfalseDisable the effect
eventsElHTMLElement | nullnullElement to listen for events on

Events

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);

Methods

instance.enable(); // Re-enable after disable
instance.disable(); // Pause and reset
instance.destroy(); // Full cleanup
await instance.requestPermission(); // Manual gyroscope permission

How It Works

Most 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:

  1. Pointer or accelerometer input fires → JS computes the tilt angle
  2. Updates are coalesced via requestAnimationFrame — even if the mouse fires 1000 events/sec, only one DOM update happens per frame
  3. JS sets --levita-x and --levita-y as CSS custom properties on the element
  4. A CSS transform rule reads those properties, and transition smooths 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

Comparison

FeatureLevitaAtroposvanilla-tilt
Bundle size (gzip)~2KB~2KB~3-4KB
Animation strategyCSS custom propertiesCSS transitionsrAF loop
Tree-shakeable
Multi-layer parallax✅ (data attrs)✅ (data attrs)
AccelerometerAuto + manual (calibrated)Partial (no calibration)
Grouped instances✅ (eventsEl)✅ (stretchX/Y/Z)✅ (mouse-event-element)
Runtime option update✅ (update())
ReactOfficial wrapperOfficial wrapperCommunity
VueOfficial wrapperWeb ComponentCommunity
SvelteOfficial wrapper
AngularOfficial wrapperWeb ComponentCommunity
TypeScriptNative (source in TS)Declaration fileDefinitelyTyped
Last published202620232021

Benchmarks

Measured with Vitest bench (happy-dom):

Scenarioops/s
Basic init + destroybench
Init with glare + shadowbench
Init with 5 parallax layersbench
Pointer move updatebench
Pointer move with glare + shadowbench

Run locally: pnpm bench

Automatically updated at each release.

Contributing

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.

Sponsors

If you find Levita useful, consider supporting its development:

Buy Me a Coffee GitHub Sponsors

Star History

Star History Chart

Roadmap

Interested in what's coming next? You can follow the development progress and upcoming features on our public project board:

Levita Project Board

License

MIT