Parallax Layers
Original · freeA layered depth scene where background, midground and foreground drift at different speeds, driven by scroll position.
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"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
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 quoteSimilar components
Browse all →
Scroll Progress Reveal
OriginalA section that reveals its content in sequence as you scroll, with a sticky scroll-progress bar showing how far through it you are.

Pinned Scroll Steps
OriginalA pinned, sticky panel whose visual and copy change step by step as you scroll past it, a classic scrollytelling section.

Horizontal Scroll Gallery
OriginalVertical scrolling pans a horizontal rail of cards sideways while the section is pinned, then releases.

Scroll Reveal Timeline
OriginalA vertical timeline whose connecting line fills and whose milestones fade in as they enter the viewport.

Count-up Stats Band
OriginalA metrics band whose numbers count up from zero the moment it scrolls into view.

Clip-path Image Reveal
OriginalPanels that unmask with a clip-path wipe as they enter the viewport, revealing the media beneath.

Stacking Cards
OriginalA deck of cards that pin and scale as you scroll, each settling behind the next.

Scroll Text Highlight
OriginalA statement whose words light up one by one as you scroll through the section.

Scroll Zoom Hero
OriginalA hero whose backdrop scales and brightens while the headline parallaxes upward as you scroll.

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

