Scroll Progress Reveal
Original · freeA section that reveals its content in sequence as you scroll, with a sticky scroll-progress bar showing how far through it you are.
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-progress-reveal.json"use client";
import { useRef } from "react";
import type { ReactNode } from "react";
import {
motion,
useScroll,
useSpring,
useTransform,
useMotionValueEvent,
} from "motion/react";
import { useState } from "react";
type Feature = {
index: string;
title: string;
blurb: string;
meta: string;
icon: ReactNode;
accent: string;
glow: string;
};
const FEATURES: Feature[] = [
{
index: "01",
title: "Edge-rendered in 40ms",
blurb:
"Every request resolves at the nearest of 320 points of presence, so a reader in Jakarta and a reader in Lisbon both see paint before they blink.",
meta: "Global CDN",
accent: "text-indigo-500 dark:text-indigo-400",
glow: "from-indigo-500/25 to-sky-500/10",
icon: (
<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 0c2.5 2.5 3.75 6.25 3.75 10S14.5 19.5 12 22m0-20C9.5 4.5 8.25 8.25 8.25 12S9.5 19.5 12 22M2 12h20" />
),
},
{
index: "02",
title: "Type-safe end to end",
blurb:
"Schemas flow from the database through the API to the component props. A rename in one file lights up every downstream mistake before you ever hit save.",
meta: "Developer experience",
accent: "text-violet-500 dark:text-violet-400",
glow: "from-violet-500/25 to-fuchsia-500/10",
icon: (
<>
<path d="m8 9-3 3 3 3" />
<path d="m16 9 3 3-3 3" />
<path d="M13.5 6.5 10.5 17.5" />
</>
),
},
{
index: "03",
title: "Zero-config previews",
blurb:
"Open a pull request and a fully isolated environment spins up with its own database branch. Share the link, gather feedback, throw it away when you merge.",
meta: "Ship faster",
accent: "text-emerald-500 dark:text-emerald-400",
glow: "from-emerald-500/25 to-teal-500/10",
icon: (
<>
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M6 9v6" />
<path d="M18 9a9 9 0 0 1-9 9" />
</>
),
},
{
index: "04",
title: "Observability, built in",
blurb:
"Traces, logs, and vitals arrive without an agent or a sidecar. Click any slow request and walk backward through every span that made it slow.",
meta: "See everything",
accent: "text-amber-500 dark:text-amber-400",
glow: "from-amber-500/25 to-orange-500/10",
icon: (
<>
<path d="M3 12h4l2 6 4-14 2 8h6" />
</>
),
},
{
index: "05",
title: "Rollbacks that mean it",
blurb:
"A bad deploy is one keystroke from undone. Traffic shifts back to the last healthy build atomically, so no user ever straddles two versions of your app.",
meta: "Sleep at night",
accent: "text-fuchsia-500 dark:text-fuchsia-400",
glow: "from-fuchsia-500/25 to-pink-500/10",
icon: (
<>
<path d="M3 7v6h6" />
<path d="M3 13a9 9 0 1 0 3-7.7L3 7" />
</>
),
},
];
function FeatureRow({ feature, i }: { feature: Feature; i: number }) {
const flip = i % 2 === 1;
return (
<motion.article
initial={{ opacity: 0, y: 56 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
className="group relative"
>
<div
className={[
"relative flex flex-col gap-6 overflow-hidden rounded-3xl border p-7 sm:p-9",
"border-slate-200/70 bg-white/70 backdrop-blur-sm",
"dark:border-white/10 dark:bg-white/[0.03]",
"shadow-[0_1px_0_0_rgba(255,255,255,0.6)_inset,0_20px_50px_-30px_rgba(15,23,42,0.35)]",
"dark:shadow-[0_1px_0_0_rgba(255,255,255,0.06)_inset,0_30px_60px_-40px_rgba(0,0,0,0.9)]",
"md:flex-row md:items-center md:gap-10",
flip ? "md:flex-row-reverse" : "",
].join(" ")}
>
{/* soft accent glow */}
<div
aria-hidden="true"
className={[
"pointer-events-none absolute -top-24 h-56 w-56 rounded-full bg-gradient-to-br blur-3xl",
"opacity-60 transition-opacity duration-500 group-hover:opacity-100",
feature.glow,
flip ? "-left-24" : "-right-24",
].join(" ")}
/>
{/* visual */}
<div className="relative shrink-0">
<div
className={[
"relative grid h-24 w-24 place-items-center rounded-2xl sm:h-28 sm:w-28",
"border border-slate-200/80 bg-gradient-to-br from-white to-slate-50",
"dark:border-white/10 dark:from-white/[0.08] dark:to-transparent",
].join(" ")}
>
<div
aria-hidden="true"
className={[
"absolute inset-0 rounded-2xl bg-gradient-to-br opacity-40",
feature.glow,
].join(" ")}
/>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
className={["relative h-10 w-10", feature.accent].join(" ")}
aria-hidden="true"
>
{feature.icon}
</svg>
<span
className="absolute -bottom-3 -right-3 grid h-9 w-9 place-items-center rounded-xl bg-slate-900 text-[11px] font-semibold tracking-wider text-white shadow-lg ring-1 ring-white/10 dark:bg-white dark:text-slate-900"
aria-hidden="true"
>
{feature.index}
</span>
</div>
</div>
{/* copy */}
<div className="relative flex-1">
<span
className={[
"mb-3 inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-[11px] font-medium uppercase tracking-widest",
"border-slate-200 bg-slate-50 text-slate-500",
"dark:border-white/10 dark:bg-white/5 dark:text-slate-400",
].join(" ")}
>
<span className={["h-1.5 w-1.5 rounded-full bg-current", feature.accent].join(" ")} />
{feature.meta}
</span>
<h3 className="text-balance text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
{feature.title}
</h3>
<p className="mt-3 max-w-xl text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-300">
{feature.blurb}
</p>
</div>
</div>
</motion.article>
);
}
export default function ScrollProgressReveal() {
const sectionRef = useRef<HTMLElement | null>(null);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start start", "end end"],
});
const progress = useSpring(scrollYProgress, {
stiffness: 120,
damping: 30,
mass: 0.4,
});
const barWidth = useTransform(progress, [0, 1], ["0%", "100%"]);
const railHeight = useTransform(progress, [0, 1], ["0%", "100%"]);
const [percent, setPercent] = useState(0);
useMotionValueEvent(progress, "change", (v) => {
setPercent(Math.round(Math.min(1, Math.max(0, v)) * 100));
});
return (
<section
ref={sectionRef}
className="relative w-full overflow-hidden bg-slate-50 px-6 py-24 sm:py-32 dark:bg-slate-950"
>
<style>{`
@keyframes szh-drift {
0% { transform: translate3d(0,0,0) scale(1); }
50% { transform: translate3d(2%, -3%, 0) scale(1.06); }
100% { transform: translate3d(0,0,0) scale(1); }
}
@keyframes szh-pulse {
0%, 100% { opacity: .35; }
50% { opacity: .75; }
}
.szh-drift { animation: szh-drift 18s ease-in-out infinite; }
.szh-pulse { animation: szh-pulse 3.2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.szh-drift, .szh-pulse { animation: none !important; }
}
`}</style>
{/* background field */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="szh-drift absolute left-1/2 top-0 h-[36rem] w-[36rem] -translate-x-1/2 rounded-full bg-gradient-to-br from-indigo-400/20 via-violet-400/10 to-transparent blur-3xl dark:from-indigo-600/20 dark:via-violet-600/10" />
<div
className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
style={{
backgroundImage:
"linear-gradient(to right, rgba(100,116,139,0.12) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,0.12) 1px, transparent 1px)",
backgroundSize: "56px 56px",
maskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, black 20%, transparent 75%)",
WebkitMaskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, black 20%, transparent 75%)",
}}
/>
</div>
{/* top sticky progress bar */}
<div className="sticky top-0 z-30 -mx-6 mb-16 px-6">
<div className="mx-auto max-w-6xl">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-slate-200/80 dark:bg-white/10">
<motion.div
style={{ width: barWidth }}
className="szh-pulse h-full rounded-full bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500 shadow-[0_0_12px_rgba(139,92,246,0.6)]"
/>
</div>
</div>
</div>
<div className="relative mx-auto grid max-w-6xl grid-cols-1 gap-12 lg:grid-cols-[1fr_minmax(0,2fr)]">
{/* sticky rail column */}
<div className="lg:relative">
<div className="lg:sticky lg:top-28">
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/60 px-3 py-1 text-xs font-medium uppercase tracking-widest text-slate-500 backdrop-blur dark:border-white/10 dark:bg-white/5 dark:text-slate-400">
<span className="szh-pulse h-2 w-2 rounded-full bg-emerald-500" />
The platform
</p>
<h2 className="text-balance text-4xl font-semibold tracking-tight text-slate-900 sm:text-5xl dark:text-white">
Everything ships,
<span className="bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500 bg-clip-text text-transparent">
{" "}
nothing breaks.
</span>
</h2>
<p className="mt-4 max-w-md text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-300">
Scroll through the pillars that keep fast-moving teams calm. The rail
on the left tracks exactly how far you have read.
</p>
{/* vertical rail + live percent */}
<div className="mt-8 flex items-center gap-4">
<div className="relative h-40 w-1.5 overflow-hidden rounded-full bg-slate-200/80 dark:bg-white/10">
<motion.div
style={{ height: railHeight }}
className="absolute inset-x-0 top-0 rounded-full bg-gradient-to-b from-indigo-500 via-violet-500 to-fuchsia-500"
/>
</div>
<div className="tabular-nums">
<div className="text-3xl font-semibold tracking-tight text-slate-900 dark:text-white">
{percent}
<span className="text-lg text-slate-400 dark:text-slate-500">%</span>
</div>
<div className="text-xs uppercase tracking-widest text-slate-400 dark:text-slate-500">
read
</div>
</div>
</div>
</div>
</div>
{/* feature rows */}
<div className="flex flex-col gap-6 sm:gap-8">
{FEATURES.map((f, i) => (
<FeatureRow key={f.index} feature={f} i={i} />
))}
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.6 }}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
className="relative overflow-hidden rounded-3xl bg-slate-900 p-9 text-center dark:bg-white/[0.04] dark:ring-1 dark:ring-white/10"
>
<div
aria-hidden="true"
className="szh-pulse pointer-events-none absolute inset-0 bg-gradient-to-br from-indigo-500/30 via-transparent to-fuchsia-500/20"
/>
<h3 className="relative text-2xl font-semibold tracking-tight text-white sm:text-3xl">
You made it to the end.
</h3>
<p className="relative mx-auto mt-3 max-w-md text-pretty text-slate-300">
Five pillars, one platform. Start building on a stack that gets out
of your way.
</p>
<a
href="#"
className="relative mt-6 inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 text-sm font-semibold text-slate-900 transition-transform duration-200 hover:scale-[1.03]"
>
Start free
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
aria-hidden="true"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a>
</motion.div>
</div>
</div>
</section>
);
}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 →
Parallax Layers
OriginalA layered depth scene where background, midground and foreground drift at different speeds, driven by scroll position.

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.

