>spinlap
Writing5 min read

Zero-Gravity State: Managing Complex Animations in React Layouts

Published on April 20, 2026

Zero-Gravity State in Web Apps

Building animations that feel weightless—where cards tilt, particle fields drift, and skill bubbles orbit—requires a shift in how we think about React state updates. If we store high-frequency cursor coordinates or frame-by-frame physics loops inside React component state (useState), we trigger thousands of unnecessary re-renders.

This leads to interface stuttering, input lag, and a poor user experience.

To solve this, we must adopt a zero-gravity state architecture.

The Rules of Zero-Gravity State

  1. Bypass the React Render Cycle: For high-frequency updates (like custom cursors tracking the mouse or Canvas animations), store values in React useRef rather than state.
  2. Utilize RequestAnimationFrame (rAF): Read directly from mutable refs inside a requestAnimationFrame loop.
  3. Hardware Acceleration: Shift layout transitions and transforms to the GPU using CSS will-change: transform and transform-gpu tailwind properties.
code compiler
┌──────────────────────────────────────────────────────────────┐
│                         Browser DOM                          │
│                                                              │
│  ┌───────────────────────┐         ┌──────────────────────┐  │
│  │   High-Freq Mouse     ├────────►│   React Refs (X, Y)  │  │
│  │   Move Event Listener │         └──────────┬───────────┘  │
│  └───────────────────────┘                    │              │
│                                               ▼              │
│  ┌───────────────────────┐         ┌──────────────────────┐  │
│  │   DOM Elements /      │◄────────┤ requestAnimationFrame│  │
│  │   Canvas Shader       │         │ Loop (with LERP lag) │  │
│  └───────────────────────┘         └──────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Implementing the LERP Lag

Linear interpolation (LERP) is the secret behind custom cursors or smooth scrolling that feel like they drift. Instead of snapping elements directly to coordinates, we move them a percentage of the remaining distance:

$$\text += (\text - \text) \times \text$$

Here is a clean typescript utility implementing this loop for a dual-axis translation:

code compiler
interface Point {
  x: number;
  y: number;
}

export class SmoothTracker {
  private current: Point = { x: 0, y: 0 };
  private target: Point = { x: 0, y: 0 };
  private factor: number;

  constructor(factor: number = 0.1) {
    this.factor = factor;
  }

  public setTarget(x: number, y: number) {
    this.target = { x, y };
  }

  public update(): Point {
    this.current.x += (this.target.x - this.current.x) * this.factor;
    this.current.y += (this.target.y - this.current.y) * this.factor;
    return { ...this.current };
  }
}

By separating structural application state (handled by React) from high-frequency rendering updates (handled by direct refs and native loops), we can achieve smooth interfaces that run at a lock 120fps.