Tailwind UI Pattern Registry for humans and agents

scroll-linked progress-bar parallax motion-one inview scroll-callback motion-one

Scroll Progress

Scroll Progress

Reading progress bar, parallax layer, and entrance cards — scroll() drives a progress callback, inView() + animate() handles reveals.

Motion One Scroll
Live Preview

Dependencies

Motion v11.11.13
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Scroll Progress — Motion Recipe</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://cdn.jsdelivr.net/npm/motion@11.11.13/dist/motion.js"></script>
  <style>
    #progress-bar {
      transform-origin: left center;
      transform: scaleX(0);
    }
    .reveal-card {
      opacity: 0;
    }
    @media (prefers-reduced-motion: reduce) {
      #progress-bar { display: none; }
      .reveal-card { opacity: 1 !important; transform: none !important; }
    }
  </style>
</head>
<body class="bg-slate-950 text-slate-200 font-sans">

  <!-- Sticky progress bar -->
  <div class="sticky top-0 z-50 h-[3px] bg-slate-800/60">
    <div id="progress-bar" class="h-full bg-gradient-to-r from-violet-500 via-fuchsia-400 to-pink-500"></div>
  </div>

  <!-- Hero -->
  <div class="mx-auto max-w-3xl px-8 pt-16 pb-20 text-center">
    <span class="mb-5 inline-block rounded-full border border-violet-500/20 bg-violet-500/10 px-3.5 py-1 text-[0.7rem] font-semibold uppercase tracking-widest text-violet-400">Motion · scroll()</span>
    <h1 class="mb-5 text-4xl font-black leading-[1.1] text-slate-100 sm:text-5xl">Scroll-linked<br>animations</h1>
    <p class="mx-auto max-w-md text-base leading-relaxed text-slate-400">The gradient bar above tracks your scroll position. Cards below reveal with <code class="rounded bg-slate-800 px-1.5 py-0.5 font-mono text-xs text-violet-300">inView()</code> + <code class="rounded bg-slate-800 px-1.5 py-0.5 font-mono text-xs text-violet-300">animate()</code>.</p>
    <div class="mt-8 flex items-center justify-center gap-2 text-sm text-slate-500">
      <svg class="h-4 w-4 animate-bounce text-violet-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
      Scroll down
    </div>
  </div>

  <!-- Scroll counter + parallax -->
  <div class="mx-auto max-w-3xl px-8 pb-20">
    <p class="mb-5 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">scroll() — progress callback</p>
    <div class="flex gap-6 rounded-2xl border border-white/[0.06] bg-white/[0.03] p-8">
      <div class="text-center shrink-0">
        <p class="text-6xl font-black tabular-nums text-violet-400 leading-none" id="scroll-pct">0</p>
        <p class="mt-2 text-xs font-semibold uppercase tracking-widest text-slate-500">% scrolled</p>
      </div>
      <div>
        <p class="text-sm font-semibold text-slate-200 mb-2">One callback, three effects</p>
        <p class="text-sm leading-relaxed text-slate-400">
          <code class="rounded bg-slate-800 px-1 font-mono text-xs text-violet-300">scroll(fn)</code> calls <code class="rounded bg-slate-800 px-1 font-mono text-xs text-violet-300">fn</code> with a <code class="rounded bg-slate-800 px-1 font-mono text-xs text-violet-300">progress</code> value (0–1) on every scroll frame. The progress bar, parallax orb, and this counter all update from the same single callback.
        </p>
      </div>
    </div>
  </div>

  <!-- Parallax section -->
  <div class="mx-auto max-w-3xl px-8 pb-20">
    <p class="mb-5 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">scroll() — parallax</p>
    <div class="relative rounded-2xl border border-white/[0.06] bg-white/[0.03] overflow-hidden" style="height:200px">
      <div id="parallax-orb" class="absolute inset-0 flex items-center justify-center pointer-events-none" aria-hidden="true">
        <div class="h-48 w-48 rounded-full bg-gradient-to-br from-violet-600/40 to-fuchsia-600/30 blur-3xl"></div>
      </div>
      <div class="absolute inset-0 flex flex-col items-center justify-center text-center px-6">
        <p class="relative text-lg font-black text-slate-100">Glow drifts upward</p>
        <p class="relative mt-2 text-sm text-slate-500">Moves at 40% of page scroll speed via <code class="rounded bg-slate-800 px-1 font-mono text-xs text-violet-300">progress × −80px</code></p>
      </div>
    </div>
  </div>

  <!-- inView + animate cards -->
  <div class="mx-auto max-w-3xl px-8 pb-20">
    <p class="mb-5 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">inView() + animate() — entrance</p>
    <div class="grid gap-4 sm:grid-cols-2">
      <div class="reveal-card rounded-xl border border-white/[0.06] bg-white/[0.03] p-6">
        <p class="mb-2 text-sm font-semibold text-slate-100">inView(selector, callback)</p>
        <p class="text-xs leading-relaxed text-slate-500">Fires once when the element enters the viewport. Return the cleanup function to auto-disconnect after first play.</p>
      </div>
      <div class="reveal-card rounded-xl border border-white/[0.06] bg-white/[0.03] p-6">
        <p class="mb-2 text-sm font-semibold text-slate-100">animate(target, keyframes)</p>
        <p class="text-xs leading-relaxed text-slate-500">Drives WAAPI directly. Pass a selector string, element, or NodeList. Keyframes use the same syntax as CSS.</p>
      </div>
      <div class="reveal-card rounded-xl border border-white/[0.06] bg-white/[0.03] p-6">
        <p class="mb-2 text-sm font-semibold text-slate-100">easing: [0.22, 1, 0.36, 1]</p>
        <p class="text-xs leading-relaxed text-slate-500">Cubic-bezier array shorthand. Fast in, soft landing — a good default for entrance animations.</p>
      </div>
      <div class="reveal-card rounded-xl border border-white/[0.06] bg-white/[0.03] p-6">
        <p class="mb-2 text-sm font-semibold text-slate-100">margin option</p>
        <p class="text-xs leading-relaxed text-slate-500"><code class="font-mono text-violet-300">margin: '0px 0px -15% 0px'</code> shrinks the root margin — fires when the element is 15% inside the viewport, not at the edge.</p>
      </div>
    </div>
  </div>

  <div class="mx-auto max-w-3xl px-8 pb-24">
    <p class="mb-5 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">The code</p>
    <div class="rounded-xl border border-white/[0.06] bg-slate-900 p-5 font-mono text-[0.78rem] leading-7 text-slate-400">
      <p><span class="text-slate-500">// scroll() — progress 0–1 in callback</span></p>
      <p><span class="text-violet-400">scroll</span><span class="text-slate-500">(function(</span><span class="text-fuchsia-400">p</span><span class="text-slate-500">) {</span></p>
      <p class="pl-4">bar<span class="text-slate-500">.</span>style<span class="text-slate-500">.</span>transform <span class="text-slate-500">=</span> <span class="text-amber-300">'scaleX('</span> <span class="text-slate-500">+</span> p <span class="text-slate-500">+</span> <span class="text-amber-300">')'</span><span class="text-slate-500">;</span></p>
      <p class="pl-4">orb<span class="text-slate-500">.</span>style<span class="text-slate-500">.</span>transform <span class="text-slate-500">=</span> <span class="text-amber-300">'translateY('</span> <span class="text-slate-500">+ (</span><span class="text-amber-300">-80</span> <span class="text-slate-500">*</span> p<span class="text-slate-500">) +</span> <span class="text-amber-300">'px)'</span><span class="text-slate-500">;</span></p>
      <p class="pl-4">pct<span class="text-slate-500">.</span>textContent <span class="text-slate-500">=</span> Math<span class="text-slate-500">.</span>round<span class="text-slate-500">(</span>p <span class="text-slate-500">*</span> <span class="text-amber-300">100</span><span class="text-slate-500">);</span></p>
      <p><span class="text-slate-500">});</span></p>
      <p class="mt-4"><span class="text-slate-500">// inView() + animate() — entrance on scroll</span></p>
      <p><span class="text-violet-400">inView</span><span class="text-slate-500">(</span><span class="text-amber-300">'.reveal-card'</span><span class="text-slate-500">,</span> function<span class="text-slate-500">(</span><span class="text-fuchsia-400">info</span><span class="text-slate-500">) {</span></p>
      <p class="pl-4"><span class="text-violet-400">animate</span><span class="text-slate-500">(</span>info<span class="text-slate-500">.</span>target<span class="text-slate-500">, {</span> <span class="text-fuchsia-400">opacity</span><span class="text-slate-500">: [</span><span class="text-amber-300">0, 1</span><span class="text-slate-500">],</span> <span class="text-fuchsia-400">y</span><span class="text-slate-500">: [</span><span class="text-amber-300">20, 0</span><span class="text-slate-500">] },</span></p>
      <p class="pl-4"><span class="text-slate-500">  {</span> <span class="text-fuchsia-400">duration</span><span class="text-slate-500">:</span> <span class="text-amber-300">0.5</span><span class="text-slate-500">,</span> <span class="text-fuchsia-400">easing</span><span class="text-slate-500">: [</span><span class="text-amber-300">0.22, 1, 0.36, 1</span><span class="text-slate-500">] });</span></p>
      <p><span class="text-slate-500">}, {</span> <span class="text-fuchsia-400">margin</span><span class="text-slate-500">:</span> <span class="text-amber-300">'0px 0px -15% 0px'</span> <span class="text-slate-500">});</span></p>
    </div>
  </div>

  <div class="h-16"></div>

  <script>
    var { animate, scroll, inView } = Motion;

    var progressBar = document.getElementById('progress-bar');
    var parallaxOrb = document.getElementById('parallax-orb');
    var pctEl = document.getElementById('scroll-pct');
    var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    // scroll() callback receives progress as a plain number 0–1
    scroll(function(progress) {
      progressBar.style.transform = 'scaleX(' + progress + ')';
      if (!reduced) parallaxOrb.style.transform = 'translateY(' + (-80 * progress) + 'px)';
      if (pctEl) pctEl.textContent = String(Math.round(progress * 100));
    });

    // inView() + animate() for card entrances
    if (!reduced) {
      inView('.reveal-card', function(info) {
        animate(info.target, { opacity: [0, 1], y: [20, 0] }, {
          duration: 0.5,
          easing: [0.22, 1, 0.36, 1],
        });
      }, { margin: '0px 0px -15% 0px' });
    }
  </script>
</body>
</html>

scroll(callback) receives a progress value from 0 to 1 on every scroll frame. A single callback drives the progress bar (scaleX), the parallax offset (translateY), and the percentage counter — no RAF loop needed.

Progress barscaleX(progress) with transform-origin: left center. The CSS initial state sets transform: scaleX(0) to prevent a flash before the script runs.

Parallaxprogress × −80px moves the orb upward at 40% of the page scroll speed. Keep the max offset below the container height to avoid the element drifting out of view.

Card revealsinView('.reveal-card', callback) fires once per element when it enters the viewport. The margin: '0px 0px -15% 0px' option delays firing until the element is 15% inside the visible area, giving the animation room to play before the element is fully in view.