Web InnoventixFreeCode

Parallax Layers

Original · free

A layered depth scene where background, midground and foreground drift at different speeds, driven by scroll position.

byWeb InnoventixReact + Tailwind
scrollparallaxdepth
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/scroll-parallax-layers.json
scroll-parallax-layers.tsx
"use client";

import { useRef } from "react";
import type { ReactNode } from "react";
import {
  motion,
  useScroll,
  useTransform,
  useSpring,
  type MotionValue,
} from "motion/react";

type ParallaxProps = {
  value: MotionValue<number>;
  className?: string;
  children?: ReactNode;
  ariaHidden?: boolean;
};

function ParallaxLayer({
  value,
  className = "",
  children,
  ariaHidden = true,
}: ParallaxProps) {
  return (
    <motion.div
      aria-hidden={ariaHidden || undefined}
      style={{ y: value }}
      className={className}
    >
      {children}
    </motion.div>
  );
}

export default function ScrollParallaxLayers() {
  const containerRef = useRef<HTMLDivElement>(null);

  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ["start start", "end end"],
  });

  const smooth = useSpring(scrollYProgress, {
    stiffness: 90,
    damping: 26,
    mass: 0.5,
  });

  // Far background: drifts slowly downward (deepest, least movement in scene terms)
  const farY = useTransform(smooth, [0, 1], ["-6%", "18%"]);
  // Grid plane: drifts up, medium-slow
  const gridY = useTransform(smooth, [0, 1], ["8%", "-20%"]);
  // Midground blob cluster A: drifts up faster
  const midAY = useTransform(smooth, [0, 1], ["18%", "-42%"]);
  // Midground blob cluster B: drifts down, opposing motion
  const midBY = useTransform(smooth, [0, 1], ["-14%", "34%"]);
  // Foreground shards: fastest layer
  const foreY = useTransform(smooth, [0, 1], ["30%", "-70%"]);
  // Headline: slowest of all — the anchor of the scene
  const headlineY = useTransform(smooth, [0, 1], ["-2%", "6%"]);
  // Subtle opacity fade for chrome + progress indicators
  const chromeOpacity = useTransform(
    smooth,
    [0, 0.12, 0.88, 1],
    [0, 1, 1, 0.35]
  );
  const progressScaleX = smooth;

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
      <style>{`
        @keyframes szh-drift {
          0%   { transform: translate3d(0,0,0) scale(1); }
          50%  { transform: translate3d(2.5%, -3%, 0) scale(1.06); }
          100% { transform: translate3d(0,0,0) scale(1); }
        }
        @keyframes szh-drift-alt {
          0%   { transform: translate3d(0,0,0) scale(1.04); }
          50%  { transform: translate3d(-3%, 2.5%, 0) scale(0.97); }
          100% { transform: translate3d(0,0,0) scale(1.04); }
        }
        @keyframes szh-twinkle {
          0%, 100% { opacity: 0.25; }
          50%      { opacity: 0.9; }
        }
        @keyframes szh-hint {
          0%, 100% { transform: translateY(0); opacity: 0.5; }
          50%      { transform: translateY(6px); opacity: 1; }
        }
        .szh-blob-a { animation: szh-drift 14s ease-in-out infinite; }
        .szh-blob-b { animation: szh-drift-alt 18s ease-in-out infinite; }
        .szh-blob-c { animation: szh-drift 22s ease-in-out infinite; }
        .szh-star   { animation: szh-twinkle 4s ease-in-out infinite; }
        .szh-hint   { animation: szh-hint 1.8s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .szh-blob-a, .szh-blob-b, .szh-blob-c,
          .szh-star, .szh-hint {
            animation: none !important;
          }
        }
      `}</style>

      {/* Tall scroll driver — pins the stage while the layers drift */}
      <div ref={containerRef} className="relative h-[320vh]">
        <div className="sticky top-0 flex h-screen w-full items-center justify-center overflow-hidden">
          {/* ---------- Layer 0: base wash ---------- */}
          <div
            aria-hidden
            className="absolute inset-0 bg-gradient-to-b from-indigo-100 via-slate-50 to-violet-100 dark:from-slate-950 dark:via-slate-900 dark:to-indigo-950"
          />

          {/* ---------- Layer 1: far background glow + stars ---------- */}
          <ParallaxLayer
            value={farY}
            className="absolute inset-0 will-change-transform"
          >
            <div
              className="szh-blob-c absolute left-1/2 top-[8%] h-[70vmin] w-[70vmin] -translate-x-1/2 rounded-full opacity-70 blur-[90px]"
              style={{
                background:
                  "radial-gradient(circle at 50% 50%, rgba(99,102,241,0.55), rgba(99,102,241,0) 70%)",
              }}
            />
            {/* faint starfield */}
            {STARS.map((s, i) => (
              <span
                key={i}
                className="szh-star absolute rounded-full bg-slate-500 dark:bg-white"
                style={{
                  left: `${s.x}%`,
                  top: `${s.y}%`,
                  width: `${s.r}px`,
                  height: `${s.r}px`,
                  animationDelay: `${s.d}s`,
                }}
              />
            ))}
          </ParallaxLayer>

          {/* ---------- Layer 2: perspective grid plane ---------- */}
          <ParallaxLayer
            value={gridY}
            className="absolute inset-x-0 bottom-0 h-[75%] will-change-transform"
          >
            <div
              className="absolute inset-0 opacity-[0.28] dark:opacity-40"
              style={{
                backgroundImage:
                  "linear-gradient(to right, rgba(99,102,241,0.6) 1px, transparent 1px), linear-gradient(to bottom, rgba(139,92,246,0.6) 1px, transparent 1px)",
                backgroundSize: "56px 56px",
                maskImage:
                  "radial-gradient(ellipse 80% 60% at 50% 100%, black 20%, transparent 75%)",
                WebkitMaskImage:
                  "radial-gradient(ellipse 80% 60% at 50% 100%, black 20%, transparent 75%)",
              }}
            />
          </ParallaxLayer>

          {/* ---------- Layer 3: midground blob cluster A ---------- */}
          <ParallaxLayer
            value={midAY}
            className="absolute inset-0 will-change-transform"
          >
            <div
              className="szh-blob-a absolute left-[10%] top-[18%] h-[42vmin] w-[42vmin] rounded-full opacity-80 blur-[64px]"
              style={{
                background:
                  "radial-gradient(circle at 40% 40%, rgba(217,70,239,0.7), rgba(217,70,239,0) 70%)",
              }}
            />
            <div
              className="szh-blob-b absolute right-[8%] top-[30%] h-[36vmin] w-[36vmin] rounded-full opacity-80 blur-[60px]"
              style={{
                background:
                  "radial-gradient(circle at 60% 40%, rgba(56,189,248,0.7), rgba(56,189,248,0) 70%)",
              }}
            />
          </ParallaxLayer>

          {/* ---------- Layer 4: midground blob cluster B (opposing) ---------- */}
          <ParallaxLayer
            value={midBY}
            className="absolute inset-0 will-change-transform"
          >
            <div
              className="szh-blob-c absolute bottom-[10%] left-[24%] h-[46vmin] w-[46vmin] rounded-full opacity-70 blur-[72px]"
              style={{
                background:
                  "radial-gradient(circle at 50% 50%, rgba(16,185,129,0.6), rgba(16,185,129,0) 70%)",
              }}
            />
          </ParallaxLayer>

          {/* ---------- Layer 5: headline — moves slowest ---------- */}
          <motion.div
            style={{ y: headlineY }}
            className="relative z-20 mx-auto max-w-3xl px-6 text-center will-change-transform"
          >
            <span className="mb-5 inline-flex items-center gap-2 rounded-full border border-indigo-300/60 bg-white/70 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-700 backdrop-blur dark:border-indigo-400/30 dark:bg-white/10 dark:text-indigo-200">
              <span
                aria-hidden
                className="h-1.5 w-1.5 rounded-full bg-fuchsia-500 dark:bg-fuchsia-400"
              />
              Depth in motion
            </span>
            <h2 className="text-balance text-4xl font-black leading-[1.05] tracking-tight sm:text-6xl md:text-7xl">
              <span className="bg-gradient-to-br from-indigo-600 via-violet-600 to-fuchsia-600 bg-clip-text text-transparent dark:from-indigo-300 dark:via-violet-300 dark:to-fuchsia-300">
                Layers that breathe
              </span>
              <br />
              as you scroll
            </h2>
            <p className="mx-auto mt-6 max-w-xl text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-300 sm:text-lg">
              Five independent planes drift at their own pace, spring-smoothed
              against your scroll. The background barely stirs, the shards race
              ahead, and this headline holds its ground.
            </p>
            <div className="mt-9 flex flex-wrap items-center justify-center gap-3">
              <a
                href="#"
                className="rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-500/20 transition-transform hover:-translate-y-0.5 dark:bg-white dark:text-slate-900"
              >
                Explore the effect
              </a>
              <a
                href="#"
                className="rounded-full border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 transition-colors hover:border-slate-400 hover:bg-white/60 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-white/5"
              >
                View source
              </a>
            </div>
          </motion.div>

          {/* ---------- Layer 6: foreground shards — fastest ---------- */}
          <ParallaxLayer
            value={foreY}
            className="pointer-events-none absolute inset-0 z-30 will-change-transform"
          >
            <FloatingShard
              className="left-[6%] top-[16%] text-fuchsia-500/80 dark:text-fuchsia-400/70"
              size={64}
              rotate={-18}
            />
            <FloatingShard
              className="right-[10%] top-[22%] text-sky-500/80 dark:text-sky-400/70"
              size={44}
              rotate={24}
            />
            <FloatingShard
              className="left-[16%] bottom-[18%] text-emerald-500/80 dark:text-emerald-400/70"
              size={52}
              rotate={12}
            />
            <FloatingShard
              className="right-[14%] bottom-[24%] text-violet-500/80 dark:text-violet-400/70"
              size={38}
              rotate={-30}
            />
            <div
              className="absolute right-[22%] top-[54%] h-16 w-16 rounded-2xl border border-white/40 bg-white/20 shadow-xl backdrop-blur-md dark:border-white/10 dark:bg-white/5"
              style={{ transform: "rotate(14deg)" }}
            />
            <div
              className="absolute left-[24%] top-[40%] h-10 w-10 rounded-full border border-white/40 bg-white/20 shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-white/5"
            />
          </ParallaxLayer>

          {/* ---------- Chrome: scroll hint + progress rail ---------- */}
          <motion.div
            style={{ opacity: chromeOpacity }}
            aria-hidden
            className="absolute bottom-6 left-1/2 z-40 flex -translate-x-1/2 flex-col items-center gap-2 text-slate-500 dark:text-slate-400"
          >
            <span className="text-[11px] font-medium uppercase tracking-[0.2em]">
              Scroll
            </span>
            <svg
              className="szh-hint h-5 w-5"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
              strokeLinejoin="round"
            >
              <path d="M12 5v14" />
              <path d="m19 12-7 7-7-7" />
            </svg>
          </motion.div>

          <div
            aria-hidden
            className="absolute bottom-0 left-0 z-40 h-1 w-full bg-slate-200/50 dark:bg-white/10"
          >
            <motion.div
              style={{ scaleX: progressScaleX }}
              className="h-full w-full origin-left bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500"
            />
          </div>
        </div>
      </div>
    </section>
  );
}

type ShardProps = {
  className?: string;
  size: number;
  rotate: number;
};

function FloatingShard({ className = "", size, rotate }: ShardProps) {
  return (
    <svg
      aria-hidden
      className={`absolute drop-shadow-lg ${className}`}
      width={size}
      height={size}
      viewBox="0 0 100 100"
      fill="none"
      style={{ transform: `rotate(${rotate}deg)` }}
    >
      <path
        d="M50 4 L61 39 L96 39 L68 61 L79 96 L50 74 L21 96 L32 61 L4 39 L39 39 Z"
        fill="currentColor"
        opacity="0.85"
      />
    </svg>
  );
}

const STARS: { x: number; y: number; r: number; d: number }[] = [
  { x: 12, y: 18, r: 2, d: 0 },
  { x: 24, y: 62, r: 1.5, d: 1.2 },
  { x: 38, y: 12, r: 2.5, d: 0.6 },
  { x: 47, y: 78, r: 1.5, d: 2.1 },
  { x: 58, y: 24, r: 2, d: 0.9 },
  { x: 69, y: 66, r: 1.5, d: 1.7 },
  { x: 76, y: 14, r: 2.5, d: 0.3 },
  { x: 84, y: 48, r: 2, d: 2.4 },
  { x: 90, y: 72, r: 1.5, d: 1.1 },
  { x: 32, y: 40, r: 1.5, d: 1.9 },
  { x: 6, y: 52, r: 2, d: 0.5 },
  { x: 63, y: 86, r: 1.5, d: 1.4 },
];

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →