Prism
← All primitives

VelocitySkew

Motion

The 'liquid scroll' feel — skews its children by scroll velocity (read from Lenis), clamped and eased back to flat. No second RAF loop.

Reads Lenis velocity via useLenis; needs a ReactLenis root.

$npx shadcn@latest add https://prism.icglabs.co/r/velocity-skew.json
Dependencies:gsaplenis
View raw manifest →

components/prism/VelocitySkew.tsx
"use client";

import { useEffect, useRef } from "react";
import { useLenis } from "lenis/react";
import gsap from "gsap";

/**
 * VelocitySkew — the "liquid scroll" feel: skews its children by scroll velocity,
 * clamped and eased back to flat. Reads Lenis velocity from the existing root
 * instance (no second RAF loop) and drives a gsap.quickTo for smoothing. Honors
 * prefers-reduced-motion.
 */
export function VelocitySkew({
  children,
  className = "",
  strength = 0.5,
  max = 8,
}: {
  children: React.ReactNode;
  className?: string;
  /** velocity → degrees multiplier */
  strength?: number;
  /** clamp in degrees */
  max?: number;
}) {
  const ref = useRef<HTMLDivElement>(null);
  const skewTo = useRef<((v: number) => void) | null>(null);
  const reduced = useRef(false);

  useEffect(() => {
    reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const el = ref.current;
    if (!el || reduced.current) return;
    skewTo.current = gsap.quickTo(el, "skewY", { duration: 0.5, ease: "power3.out" });
    return () => {
      gsap.killTweensOf(el);
      skewTo.current = null;
    };
  }, []);

  useLenis((lenis: { velocity: number }) => {
    if (reduced.current || !skewTo.current) return;
    const v = Math.max(-max, Math.min(max, lenis.velocity * strength));
    skewTo.current(v);
  });

  return (
    <div ref={ref} className={className} style={{ willChange: "transform" }}>
      {children}
    </div>
  );
}
Live demo — read-only. Every section is a real, copyable primitive.