Web InnoventixFreeCode

Sticky scroll steps

Original · free

A scroll-linked how-it-works section where a sticky panel cross-fades between steps and a progress ring plus filling timeline track advance as you scroll through the process.

byWeb InnoventixReact + Tailwind
sticky scroll stepsscroll linked animationframer motion useScrollhow it works sectionanimated process timelinesticky feature section
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/afeature-sticky-scroll-steps.json
afeature-sticky-scroll-steps.tsx
"use client";

import { useRef, useState } from "react";
import { motion, AnimatePresence, useScroll, useTransform, useMotionValueEvent } from "motion/react";

const steps = [
  {
    label: "Connect",
    title: "Bring your sources together",
    body: "Link databases, spreadsheets and apps in minutes. Nothing to install, nothing to migrate, and every connection stays in sync automatically.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <path d="M9 7 5 11a4 4 0 0 0 6 6l1-1M15 17l4-4a4 4 0 0 0-6-6l-1 1M9 15l6-6" />
      </svg>
    ),
  },
  {
    label: "Map",
    title: "Design the workflow visually",
    body: "Drag steps onto a canvas and describe the logic in plain language. The flow updates live so you can see exactly how work will move.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <rect x="3" y="3" width="6" height="6" rx="1.5" /><rect x="15" y="15" width="6" height="6" rx="1.5" />
        <path d="M9 6h6a3 3 0 0 1 3 3v6" />
      </svg>
    ),
  },
  {
    label: "Automate",
    title: "Let the busywork run itself",
    body: "Triggers fire the moment conditions are met. Approvals, notifications and hand-offs happen without anyone chasing a checklist.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z" />
      </svg>
    ),
  },
  {
    label: "Refine",
    title: "Measure, then improve",
    body: "Live dashboards surface where time is spent so you can optimise the flow. Every change is versioned and easy to roll back.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6" aria-hidden="true">
        <path d="M4 20V10M10 20V4M16 20v-6M22 20H2" />
      </svg>
    ),
  },
];

export default function StickyScrollSteps() {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start start", "end end"] });
  const [active, setActive] = useState(0);

  useMotionValueEvent(scrollYProgress, "change", (v) => {
    const idx = Math.min(steps.length - 1, Math.max(0, Math.floor(v * steps.length + 0.0001)));
    setActive(idx);
  });

  const lineHeight = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
  const R = 52;
  const CIRC = 2 * Math.PI * R;
  const ringOffset = useTransform(scrollYProgress, [0, 1], [CIRC, 0]);

  return (
    <section className="relative w-full bg-white px-5 py-24 text-slate-900 dark:bg-slate-950 dark:text-white sm:px-8">
      <div className="pointer-events-none absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-indigo-50 to-transparent dark:from-indigo-950/30" />

      <header className="relative mx-auto max-w-2xl text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-600 dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
          <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
          How it works
        </span>
        <h2 className="mt-5 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
          From scattered to streamlined in four steps
        </h2>
        <p className="mx-auto mt-4 max-w-lg text-pretty text-base text-slate-600 dark:text-slate-400">
          Scroll to walk through the journey. The panel on the left keeps pace with where you are.
        </p>
      </header>

      <div ref={ref} className="relative mx-auto mt-16 max-w-6xl lg:grid lg:grid-cols-[1fr_1.05fr] lg:gap-14">
        {/* Sticky visual */}
        <div className="hidden lg:flex lg:h-screen lg:sticky lg:top-0 lg:items-center">
          <div className="relative w-full overflow-hidden rounded-3xl border border-white/10 bg-slate-900 p-8 text-white shadow-2xl">
            <div
              className="pointer-events-none absolute inset-0 opacity-90"
              style={{ background: "radial-gradient(120% 120% at 100% 0%, rgba(99,102,241,0.35), transparent 55%), radial-gradient(120% 120% at 0% 100%, rgba(217,70,239,0.3), transparent 55%)" }}
              aria-hidden="true"
            />
            <div className="relative flex items-center justify-between">
              <span className="text-sm font-medium text-white/60">
                Step {String(active + 1).padStart(2, "0")} / {String(steps.length).padStart(2, "0")}
              </span>
              <div className="relative h-16 w-16">
                <svg viewBox="0 0 120 120" className="h-16 w-16 -rotate-90">
                  <circle cx="60" cy="60" r={R} fill="none" stroke="rgba(255,255,255,0.15)" strokeWidth="8" />
                  <motion.circle
                    cx="60" cy="60" r={R} fill="none" stroke="url(#sss-grad)" strokeWidth="8" strokeLinecap="round"
                    strokeDasharray={CIRC} style={{ strokeDashoffset: ringOffset }}
                  />
                  <defs>
                    <linearGradient id="sss-grad" x1="0" y1="0" x2="1" y2="1">
                      <stop offset="0%" stopColor="#818cf8" />
                      <stop offset="100%" stopColor="#e879f9" />
                    </linearGradient>
                  </defs>
                </svg>
              </div>
            </div>

            <div className="relative mt-10 min-h-[13rem]">
              <AnimatePresence mode="wait">
                <motion.div
                  key={active}
                  initial={{ opacity: 0, y: 20 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, y: -20 }}
                  transition={{ duration: 0.4, ease: "easeOut" }}
                >
                  <span className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10 text-white ring-1 ring-white/20 backdrop-blur">
                    {steps[active].icon}
                  </span>
                  <p className="mt-6 text-7xl font-bold tracking-tighter text-white/10">
                    {String(active + 1).padStart(2, "0")}
                  </p>
                  <h3 className="-mt-8 text-2xl font-semibold tracking-tight">{steps[active].title}</h3>
                  <p className="mt-3 max-w-sm text-sm leading-relaxed text-white/70">{steps[active].body}</p>
                </motion.div>
              </AnimatePresence>
            </div>

            <div className="relative mt-8 flex gap-1.5">
              {steps.map((_, i) => (
                <span key={i} className={`h-1 flex-1 rounded-full transition-colors duration-300 ${i <= active ? "bg-indigo-400" : "bg-white/15"}`} />
              ))}
            </div>
          </div>
        </div>

        {/* Scrolling steps */}
        <div className="relative">
          <div className="absolute bottom-0 left-[19px] top-2 w-px bg-slate-200 dark:bg-white/10" />
          <motion.div style={{ height: lineHeight }} className="absolute left-[19px] top-2 w-px bg-gradient-to-b from-indigo-500 to-fuchsia-500" />

          {steps.map((s, i) => {
            const isActive = active >= i;
            return (
              <div key={i} className="relative min-h-[60vh] pl-14 lg:min-h-[75vh]">
                <span
                  className={`absolute left-[19px] top-2 flex h-6 w-6 -translate-x-1/2 items-center justify-center rounded-full border-2 transition-colors duration-300 ${isActive ? "border-indigo-500 bg-indigo-500 text-white" : "border-slate-300 bg-white text-slate-400 dark:border-white/20 dark:bg-slate-950"}`}
                >
                  <span className="h-2 w-2 rounded-full bg-current" />
                </span>
                <motion.div
                  initial={{ opacity: 0, y: 24 }}
                  whileInView={{ opacity: 1, y: 0 }}
                  viewport={{ once: true, amount: 0.5 }}
                  transition={{ duration: 0.5 }}
                  animate={{ opacity: active === i ? 1 : 0.45 }}
                  className="pt-1"
                >
                  <div className="flex items-center gap-3">
                    <span className="inline-flex h-11 w-11 items-center justify-center rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-300 lg:hidden">
                      {s.icon}
                    </span>
                    <span className="text-xs font-semibold uppercase tracking-widest text-indigo-600 dark:text-indigo-400">{s.label}</span>
                  </div>
                  <h3 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">{s.title}</h3>
                  <p className="mt-3 max-w-md text-base leading-relaxed text-slate-600 dark:text-slate-400">{s.body}</p>
                </motion.div>
              </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 →
Three Column Icon Grid

Three Column Icon Grid

Original

A responsive icon grid that presents six product capabilities as scannable cards, each with an inline SVG icon, title and short description.

Alternating Media Rows

Alternating Media Rows

Original

Three feature rows that alternate a text column with a CSS-built visual panel, walking the reader through a plan, measure and ship story.

Bento Feature Grid

Bento Feature Grid

Original

A bento-style grid mixing one large hero cell, a tall stat cell and several compact cards to give features a clear visual hierarchy.

Tabbed Feature Switcher

Tabbed Feature Switcher

Original

A tabbed feature preview that switches panels using only native radio inputs and Tailwind peer variants, so it works with no JavaScript.

Stat Backed Features

Stat Backed Features

Original

A feature layout that pairs each capability with a headline metric, so every claim is anchored to a measurable result.

Scroll-reveal bento grid

Scroll-reveal bento grid

Original

An asymmetric bento feature grid whose cards stagger into view on scroll, lift on hover and feature an animated conic gradient border, shimmering headline, growing bar chart and a masked marquee tag strip.

Spotlight hover feature cards

Spotlight hover feature cards

Original

A responsive feature card grid where each card follows the cursor with a radial spotlight glow, lifts and rotates its icon on hover, reveals a call to action, and sits above a scrolling logo marquee.

Animated marquee highlights band

Animated marquee highlights band

Original

A bold feature band with an animated gradient background, floating blurred orbs, blur-in staggered headline, two opposing marquee pill rows and shimmering stat cards.

Bordered Feature Grid (2x2)

Bordered Feature Grid (2x2)

MIT

A two-column grid of bordered feature cards, each pairing an outlined icon with a title and supporting copy. Clean, enterprise-leaning layout with full dark-mode support.

Outlined Feature Cards with CTA

Outlined Feature Cards with CTA

MIT

A three-column feature section with heading and intro, plus blue-outlined cards that each carry an icon, description, and a circular arrow call-to-action. Fully theme-aware.

Icon-Left Feature List

Icon-Left Feature List

MIT

A centered heading over a three-column feature list, each row leading with a rounded icon badge and a 'Learn More' link. Classic marketing feature block, ported with added dark variants.

Hover-Reveal Bento Grid

Hover-Reveal Bento Grid

MIT

The Magic UI bento pattern: a mixed-span grid of cards where the icon shrinks and the description lifts on hover to reveal a call-to-action. Self-contained (no framer-motion, no icon libs), pure CSS group-hover, with a glow background and full dark styling.