Web InnoventixFreeCode

Scroll Progress Reveal

Original · free

A section that reveals its content in sequence as you scroll, with a sticky scroll-progress bar showing how far through it you are.

byWeb InnoventixReact + Tailwind
scrollprogressreveal
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-progress-reveal.json
scroll-progress-reveal.tsx
"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

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 →