<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Pritam Sharma]]></title><description><![CDATA[Pritam Sharma]]></description><link>https://blog.notpritam.in</link><generator>RSS for Node</generator><lastBuildDate>Wed, 13 May 2026 07:27:17 GMT</lastBuildDate><atom:link href="https://blog.notpritam.in/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How We Built a No-Code Landing Page Editor That Ships Static Pages in Minutes]]></title><description><![CDATA[How We Built a No-Code Landing Page Editor That Ships Static Pages in Minutes
We got tired of changing hex codes for a living. So we automated ourselves out of the job.

The Old Way (Pain)
Every "smal]]></description><link>https://blog.notpritam.in/how-we-built-a-no-code-landing-page-editor-that-ships-static-pages-in-minutes</link><guid isPermaLink="true">https://blog.notpritam.in/how-we-built-a-no-code-landing-page-editor-that-ships-static-pages-in-minutes</guid><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Devops]]></category><category><![CDATA[JAMstack]]></category><category><![CDATA[cloudflare]]></category><dc:creator><![CDATA[Pritam Sharma]]></dc:creator><pubDate>Mon, 13 Apr 2026 09:51:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/65cb5c95b17a682aa46612ef/1a21cb29-907a-47bc-b148-b27d873c1e49.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>How We Built a No-Code Landing Page Editor That Ships Static Pages in Minutes</h1>
<p><em>We got tired of changing hex codes for a living. So we automated ourselves out of the job.</em></p>
<hr />
<h2>The Old Way (Pain)</h2>
<p>Every "small" landing page change went through this obstacle course:</p>
<ol>
<li><p>Marketing has an idea</p>
</li>
<li><p>Designer mocks it up (3 rounds of feedback)</p>
</li>
<li><p>Engineer builds it (next sprint, maybe)</p>
</li>
<li><p>Marketing: "Can we try a different blue?"</p>
</li>
<li><p>Back to step 2</p>
</li>
<li><p>Engineer: <em>quietly updates resume</em></p>
</li>
</ol>
<p>One landing page? Fine. Twenty pages, three languages, influencer-specific variants, each with custom pricing? We were drowning.</p>
<p>The chain was the problem: <strong>Marketing -&gt; Designer -&gt; Engineer -&gt; Designer -&gt; Engineer -&gt; Deploy</strong>. We needed: <strong>Marketing -&gt; Deploy</strong>.</p>
<img src="https://iili.io/BXzFOrX.png" alt="Before vs After workflow — 2 weeks of back-and-forth reduced to 20 minutes" style="display:block;margin:0 auto" />

<hr />
<h2>The Core Idea</h2>
<p><strong>Every landing page is a JSON file.</strong></p>
<p>Not "backed by" JSON. The JSON <em>is</em> the page — every heading, color, image, testimonial, pricing plan, CTA button. At build time, the static site generator reads the JSON and bakes it into pre-rendered HTML. No database. No CMS. No API calls at runtime.</p>
<p>Recipe goes in, cake comes out. Visitor gets the cake instantly.</p>
<img src="https://iili.io/BXzqzdX.png" alt="JSON config on the left equals a fully rendered page on the right — baked at build time" style="display:block;margin:0 auto" />

<pre><code class="language-json">{
  "hero": {
    "heading": "Build Your App 10x Faster",
    "ctaText": "Get Started Free",
    "colors": { "heading": "#FFFFFF", "ctaBackground": "#6C5CE7" }
  },
  "pricing": {
    "plans": [
      { "name": "Pro", "monthly": 29, "features": ["Unlimited projects", "Priority support"] }
    ]
  },
  "testimonials": {
    "items": [
      { "name": "Sarah Chen", "role": "CTO", "quote": "Saved us 200 engineering hours." }
    ]
  }
}
</code></pre>
<p>Change the headline? Edit one string. New testimonial? Add an object. Different pricing for France? Swap the numbers. Done.</p>
<hr />
<h2>Architecture</h2>
<img src="https://iili.io/BXz2zG9.png" alt="Architecture overview — Visual Editor to Serverless Worker to GitHub Actions to Global CDN" style="display:block;margin:0 auto" />

<p>Four pieces:</p>
<ol>
<li><p><strong>Visual Editor</strong> — Marketing customizes pages. Spits out JSON.</p>
</li>
<li><p><strong>Serverless Worker</strong> — Uploads images, validates config, triggers the build.</p>
</li>
<li><p><strong>GitHub Actions</strong> — Commits config to Git, opens a PR, deploys.</p>
</li>
<li><p><strong>Global CDN</strong> — Serves static HTML from 300+ edge locations.</p>
</li>
</ol>
<p>Marketing clicks a button. A Git commit happens. They don't need to know that.</p>
<hr />
<h2>The Editor</h2>
<img src="https://cdn.hashnode.com/uploads/covers/65cb5c95b17a682aa46612ef/72e4a1f9-b159-4a0a-ac1a-c734c62c1c42.png" alt="" style="display:block;margin:0 auto" />

<p>Three panels. That's it.</p>
<ul>
<li><p><strong>Left sidebar</strong>: 12 section editors (Hero, Pricing, Testimonials, FAQ, etc.) with fields for text, colors, images, toggles</p>
</li>
<li><p><strong>Right panel</strong>: Live preview of the actual page. Viewport switching — Desktop, Tablet, Mobile</p>
</li>
<li><p><strong>Top toolbar</strong>: Publish, presets, reset</p>
</li>
</ul>
<p>Every field maps to a path in the JSON. Change a color picker, the preview updates instantly. It's all local state — no network calls during editing.</p>
<p><strong>Auto-save to localStorage</strong> means marketing can close the browser, come back tomorrow, pick up where they left off. We also built a <strong>presets system</strong> — save configs as reusable templates. Marketing built a library of these within the first week without us asking.</p>
<hr />
<h2>The Publish Pipeline</h2>
<p>Marketing clicks "Publish." Here's what happens in the next 3 minutes:</p>
<img src="https://iili.io/BXzKitR.png" alt="The publish pipeline — from click to live in 7 automated steps" style="display:block;margin:0 auto" />

<pre><code class="language-mermaid">sequenceDiagram
    participant M as Marketing
    participant W as Worker
    participant G as GitHub Actions
    participant CDN as CDN

    M-&gt;&gt;W: Uploads images + config JSON
    W-&gt;&gt;G: Triggers build workflow
    G-&gt;&gt;G: Creates branch, commits config, opens PR
    G-&gt;&gt;CDN: Deploys to staging
    G--&gt;&gt;M: Preview URL ready
    Note over M,CDN: Engineer approves PR (~30 sec)
    G-&gt;&gt;CDN: Deploys to production
</code></pre>
<p><strong>Image upload</strong>: Scans config for images, uploads to object storage with immutable cache headers (cached for a year). Validates file types. No, marketing, you cannot upload a 50MB PSD.</p>
<p><strong>Slug generation</strong>: Auto-generates from the heading. Detects collisions, appends <code>-v2</code>, <code>-v3</code>.</p>
<p><strong>CI/CD</strong>: Creates a Git branch, writes the JSON config, commits, opens a PR, triggers deploy. All automated.</p>
<pre><code class="language-mermaid">gitGraph
    commit id: "main"
    branch variant/summer-sale
    commit id: "publish summer-sale"
    checkout main
    branch variant/influencer-alex
    commit id: "publish influencer-alex"
    checkout main
    merge variant/summer-sale
    merge variant/influencer-alex
</code></pre>
<p><strong>The zero-load-time trick</strong>: The config gets baked into static HTML at build time. When a visitor loads the page, everything is already there — colors, copy, images, pricing. No API calls. No loading spinners. The page is "done" before JavaScript even loads.</p>
<hr />
<h2>Performance</h2>
<img src="https://iili.io/BXzf6g9.png" alt="Performance comparison — runtime API calls at 2.4s vs pre-baked static at 0.3s" style="display:block;margin:0 auto" />

<p><strong>Two-tier loading</strong>: Hero, social proof, and testimonials render immediately. Pricing, FAQ, and video lazy-load via <code>IntersectionObserver</code> with 100px look-ahead. By the time you scroll there, it's ready.</p>
<p><strong>Edge delivery</strong>: Static HTML on a global CDN. No origin server. Brotli compression. Your page loads fast whether you're in Mumbai or Montreal.</p>
<p><strong>Analytics that don't block paint</strong>: PostHog and monitoring load async. Section visibility tracking fires events as sections enter the viewport — marketing gets funnel data without us sacrificing a millisecond.</p>
<hr />
<h2>Variants: Languages, Influencers, Campaigns</h2>
<p>This is where it gets fun.</p>
<p><strong>Language variants</strong>: Duplicate a preset, translate the copy, adjust pricing. Publish. Fully localized page in minutes, no i18n library needed.</p>
<p><strong>Influencer pages</strong>: Custom colors matching their brand, their testimonial front and center, tailored hero copy, unique pricing. They get a custom URL. Their audience gets a page that feels hand-built — because it is.</p>
<p><strong>Safety net</strong>: Every variant is a Git commit. Marketing published something broken? <code>git revert</code>. 30 seconds. Try doing that with a CMS.</p>
<hr />
<h2>Idea to Live Page: The Timeline</h2>
<pre><code class="language-mermaid">timeline
    title 20 Minutes, Start to Finish
    0 min : Open editor, load a preset
    3 min : Customize copy, colors, pricing, testimonials
    5 min : Preview on mobile, tweak, hit Publish
    8 min : Staging deploy complete, verify preview
    15 min : Engineer approves PR
    20 min : Live on production, globally
</code></pre>
<p>Old flow: 1-2. New flow: 20 minutes. Engineering's involvement: a 30-second PR approval.</p>
<hr />
<h2>What We'd Do Differently</h2>
<p><strong>Config bloat</strong>: Each variant JSON is ~38KB because it stores everything, including defaults. Storing only overrides would cut this by 80% and make PR diffs actually readable.</p>
<p><strong>Single-player editing</strong>: localStorage means one person per browser. Two marketers editing simultaneously = last one wins. Real-time collab would fix this, but hasn't been needed yet.</p>
<p><strong>Preview speed</strong>: The "real" preview requires a staging deploy (~3 min). An on-demand render function would make this instant.</p>
<hr />
<h2>TL;DR</h2>
<ul>
<li><p>JSON config = entire page. Git = CMS. PR = content approval.</p>
</li>
<li><p>Static generation means zero API calls at runtime. Pages load instantly.</p>
</li>
<li><p>Marketing clicks Publish, automation handles the rest. Engineering reviews at their pace.</p>
</li>
<li><p>Every variant is a Git commit. Rollbacks are a <code>git revert</code> away.</p>
</li>
</ul>
<p>Marketing went from "file a ticket and wait two weeks" to "ship it yourself in 20 minutes." Engineering went from "change hex codes" to "approve PRs over coffee."</p>
<p>Everyone's happier. Especially the engineer.</p>
<hr />
<p><em>Building something similar? Hit me up — happy to nerd out about config-driven UIs and making marketing teams dangerously autonomous.</em></p>
]]></content:encoded></item><item><title><![CDATA[Award Winning Marquee Animation with Framer Motion]]></title><description><![CDATA[Have you ever wondered, how do they create such smooth and beautiful animations in their site ? Worry not, in this article i will be guiding you through how you can create such amazing award winning effects..
Here is what we will be creating Visit De...]]></description><link>https://blog.notpritam.in/award-winning-marquee-animation-with-framer-motion</link><guid isPermaLink="true">https://blog.notpritam.in/award-winning-marquee-animation-with-framer-motion</guid><category><![CDATA[animation]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[framer-motion]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[React]]></category><category><![CDATA[scroll animation]]></category><dc:creator><![CDATA[Pritam Sharma]]></dc:creator><pubDate>Fri, 08 Mar 2024 06:47:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709737724697/b5b6dcea-caa8-492d-bcb2-a38a47cfc7fa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever wondered, how do they create such smooth and beautiful animations in their site ? Worry not, in this article i will be guiding you through how you can create such amazing award winning effects..</p>
<p>Here is what we will be creating <a target="_blank" href="https://scroll-pied.vercel.app/">Visit Demo</a>  </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/tyb62Z6wkFM">https://youtu.be/tyb62Z6wkFM</a></div>
<p> </p>
<p>Without wasting any time , let's get started,<br />I am using nextjs for this you can choose react or whatever you prefer..</p>
<pre><code class="lang-bash">╰─ npx create-next-app@latest park --tailwind
</code></pre>
<p>Add these packages in your <code>package.json</code></p>
<pre><code class="lang-json"> <span class="hljs-string">"dependencies"</span>: {
    <span class="hljs-attr">"@react-hook/window-size"</span>: <span class="hljs-string">"^3.1.1"</span>,  <span class="hljs-comment">// to get the size of window</span>
    <span class="hljs-attr">"framer-motion"</span>: <span class="hljs-string">"^11.0.8"</span>, <span class="hljs-comment">// to animate our elements</span>
    <span class="hljs-attr">"normalize-wheel"</span>: <span class="hljs-string">"^1.0.1"</span>, <span class="hljs-comment">// to normalize the wheel scroll details</span>
    <span class="hljs-attr">"react-use"</span>: <span class="hljs-string">"^17.5.0"</span> <span class="hljs-comment">// to use useRafLoop to render stuff</span>
  }
</code></pre>
<p>don't forget to run <code>npm install</code> to install these packages.</p>
<p>let's clear the boilerplate and add the constants which we will be using in <code>page.jsx</code></p>
<pre><code class="lang-javascript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">const</span> _ = {
  <span class="hljs-attr">speed</span>: <span class="hljs-number">1</span>,
  <span class="hljs-attr">threshold</span>: <span class="hljs-number">0.014</span>,
  <span class="hljs-attr">wheelFactor</span>: <span class="hljs-number">1.25</span>,
  <span class="hljs-attr">dragFactor</span>: <span class="hljs-number">1.2</span>,
};

<span class="hljs-keyword">const</span> data = [
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/VIqlDWL1UV6azfAV9xBz0MPhw.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Petrified Forest"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/L1RFqeJ6NviUiTVLT3u2pL6IU.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Mountain Valley"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/Q44lHbttA8VG6mS9allIhkYAR8Y.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Cannon Beach"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/3LKCYdYAgamssbz1JJSOnu8vM.jpg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Redwood"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/NcLJHjVnMgsxeBqzuinazFIEXoc.jpg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Brookings"</span>,
  },
];

<span class="hljs-keyword">const</span> data3 = [
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/CzfcVySzXAvy0AuiJGpGMtrpus.jpeg?scale-down-to=512"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Zion"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/I6xwwDpEjzsZzRHm0K43uSBUTFw.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Saguro"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/xzKVdjf3wAmhNYnOArbE2UDgm8.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Josha Tree"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/Q44lHbttA8VG6mS9allIhkYAR8Y.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Cannon Beach"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/q7JcwecvpKTVhzSu8X3xeEJUmM.jpg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Grand Teton"</span>,
  },
];

<span class="hljs-keyword">const</span> data2 = [
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/hTsiS6tZ2GARqbwWY5dIUHGtcU.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Yosemite"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/gLC4I3K6WZDH2d8LImM944p2oVw.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Catalina"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/fJDYXXbxmLW7qi3QCMfWXJRvPc.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Great Sand Dunes"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/xzKVdjf3wAmhNYnOArbE2UDgm8.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Sedona"</span>,
  },
  {
    <span class="hljs-attr">image</span>:
      <span class="hljs-string">"https://framerusercontent.com/images/NSExEw4QIpuVtifFprhP3QxjY.jpeg?scale-down-to=1024"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Morro Bay"</span>,
  },
];

<span class="hljs-keyword">const</span> data4 = [
  {
    <span class="hljs-attr">image</span>: <span class="hljs-string">"https://www.touropia.com/gfx/b/2020/09/badlands.jpg"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Badlands"</span>,
  },
  {
    <span class="hljs-attr">image</span>: <span class="hljs-string">"https://www.touropia.com/gfx/b/2020/09/niagara_falls.jpg"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Niagara Falls"</span>,
  },
  {
    <span class="hljs-attr">image</span>: <span class="hljs-string">"https://www.touropia.com/gfx/b/2020/09/big_sur.jpg"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Big Sur"</span>,
  },
  {
    <span class="hljs-attr">image</span>: <span class="hljs-string">"https://www.touropia.com/gfx/b/2020/09/florida_keys.jpg"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Florida Keys"</span>,
  },
  {
    <span class="hljs-attr">image</span>: <span class="hljs-string">"https://www.touropia.com/gfx/b/2020/09/manhattan.jpg"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Manhattan"</span>,
  },
];

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col max-w-screen overflow-x-hidden items-center text-center"</span>&gt;</span>
      {/* We will be adding content here  */}
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<p>now what are we going to do is to add some content in our code so it shows up.</p>
<pre><code class="lang-javascript">&lt;main className=<span class="hljs-string">"flex flex-col max-w-screen overflow-x-hidden items-center text-center"</span>&gt;
      {<span class="hljs-built_in">Array</span>.from({ <span class="hljs-attr">length</span>: <span class="hljs-number">1</span> }).map(<span class="hljs-function">(<span class="hljs-params">item, index</span>) =&gt;</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">className</span>=<span class="hljs-string">"h-screen text-[5rem] text-white font-thin w-full flex items-center justify-center bg-black"</span>
          <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span>
        &gt;</span>
          Insert Beautiful Hero Section Here
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
      ))}
      &lt;div className=<span class="hljs-string">"flex items-center justify-center flex-col gap-4 mt-[8rem] text-[4rem] max-w-[500px] font-semibold "</span>&gt;
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-[64px] text-white font-cavet"</span>&gt;</span>
          Featured journeys
        <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-[18px] text-[#808080] font-light"</span>&gt;</span>
          Each photo unveils part of a journey through America, capturing
          moments from serene beaches at dawn to the vibrant twilight over the
          Pacific.
        <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
      &lt;/div&gt;

      {<span class="hljs-built_in">Array</span>.from({ <span class="hljs-attr">length</span>: <span class="hljs-number">2</span> }).map(<span class="hljs-function">(<span class="hljs-params">item, index</span>) =&gt;</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">className</span>=<span class="hljs-string">"h-screen text-[6rem] text-white font-thin w-full flex items-center justify-center bg-black"</span>
          <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span>
        &gt;</span>
          Another Section
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
      ))}
    &lt;/main&gt;
</code></pre>
<p>it will look like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709739776399/9036eee9-6cf2-41e4-9dfe-0403d3fd55bb.png" alt class="image--center mx-auto" /></p>
<p>now let's listen to mouse wheel or scroll, so for that i am going to create a ref named <code>x</code> and a function named <code>onWheel</code> , and i am going to use <code>normalizeWheel</code> function which i will be importing from <code>normalizeWheel</code> package to get the value in formatted way and store that value in our <code>x</code> reference after multiplying by wheel factor.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> x = useRef(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> onWheel = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> normalized = normalizeWheel(e);
    x.current = normalized.pixelY * _.wheelFactor;
  };
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span>
      <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col max-w-screen overflow-x-hidden items-center text-center"</span>
      <span class="hljs-attr">onWheel</span>=<span class="hljs-string">{onWheel}</span>
    &gt;</span></span>
</code></pre>
<p>now we have our scrolling speed stored in <code>x.current</code> and whenever we scroll, it updates, and <em>it doesn't cause any render cause reference doesn't cause re-renders in react</em>.</p>
<p>now let's create two more components, either in same file or you can create separate file also i will be creating them in same file to have the all code at one place.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> MarqueeContainer = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-[30px]"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
};

<span class="hljs-keyword">const</span> MarqueeItem = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"shrink-0 flex gap-[40px] items-center  text-white  whitespace-nowrap"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};
</code></pre>
<p>i have added some basic styling to it, which you can understand by going through the code.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> MarqueeContainer = <span class="hljs-function">(<span class="hljs-params">{x, direction, springDetails, cardData}</span>) =&gt;</span> {
 <span class="hljs-keyword">var</span> initialValue = _.speed;
  <span class="hljs-keyword">if</span> (direction === <span class="hljs-string">"right"</span>) {
    initialValue *= <span class="hljs-number">-1</span>;
  }

  <span class="hljs-keyword">const</span> speed = useSpring(initialValue, springDetails);

  <span class="hljs-keyword">const</span> loop = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Math</span>.abs(x.current) &lt; _.threshold) <span class="hljs-keyword">return</span>; <span class="hljs-comment">// to preserver a minimum speed</span>

    x.current *= <span class="hljs-number">0.66</span>; <span class="hljs-comment">// so we gradiuallly decrease the speed to a threshold other wise it will infinitley speed</span>

    <span class="hljs-keyword">if</span> (direction === <span class="hljs-string">"right"</span>) {
      speed.set((_.speed + x.current) * <span class="hljs-number">-1</span>);
    } <span class="hljs-keyword">else</span> {
      speed.set(_.speed + x.current);
    }
  };

  useRafLoop(loop, <span class="hljs-literal">true</span>);
  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-[30px]"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
};
</code></pre>
<p>In <code>MarqueeContainer</code> , i have added some props named <code>direction</code> , <code>springDetails</code> and <code>x</code>, we will be using <code>direction</code> to control the direction of animation, <code>springDetails</code> will contain details regarding the springAnimations which we will pass during initialization of <code>MarqueeContainer</code> , <code>x</code> will contain our reference to speed and <code>cardData</code> is just data for rendering the card component which we will create later on.</p>
<p>in first few lines, i am checking the direction, and if it is in right, then i am multiplying it with -1 to make it in opposite direction.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> speed = useSpring(initialValue, springDetails );
</code></pre>
<p>in this line , i am using <code>useSpring</code> hook from <code>framer-motion</code> , to create a speed motion values which we will be using to control the speed of our <code>MarqueeItem</code>. you can read more about useSpring hook here.<br />Here is how <code>MarqueeContainer</code> intilization will look like at the moment.</p>
<pre><code class="lang-javascript">&lt;MarqueeContainer
    x={x}
    direction={<span class="hljs-string">"left"</span>}
    springDetails={{ <span class="hljs-attr">damping</span>: <span class="hljs-number">200</span>, <span class="hljs-attr">stiffness</span>: <span class="hljs-number">1000</span>, <span class="hljs-attr">mass</span>: <span class="hljs-number">1</span> }}
    cardData={data}
 /&gt;
</code></pre>
<p>damping is the opposing force, and stiffness is how quickly we want our changes to show. mass is just mass we define for our elements, which will be used in their internal calculations to control the animations.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> loop = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Math</span>.abs(x.current) &lt; _.threshold) <span class="hljs-keyword">return</span>; <span class="hljs-comment">// to preserver a minimum speed</span>

    x.current *= <span class="hljs-number">0.66</span>; <span class="hljs-comment">// so we gradiuallly decrease the speed to a threshold other wise it will infinitley speed</span>

    <span class="hljs-keyword">if</span> (direction === <span class="hljs-string">"right"</span>) {
      speed.set((_.speed + x.current) * <span class="hljs-number">-1</span>);
    } <span class="hljs-keyword">else</span> {
      speed.set(_.speed + x.current);
    }
  };
</code></pre>
<p>in the loop function, firstly i am checking the current speed , if it is less than threshold then i am not doing anything and returing it.<br />if it is greater than our threshold then i am mutiplying it 0.66 and storing it , so it doesn't just keep increasing.<br />now i setting the speed to <code>x.current + our initial speed</code> , and if direction is right so multiplying it by -1, so we get a reverse animation.</p>
<pre><code class="lang-javascript">useRafLoop(loop, <span class="hljs-literal">true</span>);
</code></pre>
<p>in last line i am just calling this loop again and again so it keeps updating the value. i am using <code>useRafLoop</code> hook from <code>react-use</code>.</p>
<p>now let's create our <code>MarqueeItem</code> and pass down the speed which we have been storing in speed variable.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> MarqueeItem = <span class="hljs-function">(<span class="hljs-params">{ children, speed }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> item = useRef(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> rect = useRef({});
  <span class="hljs-keyword">const</span> x = useRef(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> [width, height] = useWindowSize();

  <span class="hljs-keyword">const</span> setX = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!item.current || !rect.current) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">var</span> xPercentage = (x.current / rect.current.width) * <span class="hljs-number">100</span>;
    <span class="hljs-keyword">if</span> (xPercentage &lt; <span class="hljs-number">-100</span>) x.current = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">if</span> (xPercentage &gt; <span class="hljs-number">0</span>) x.current = -rect.current.width;

    item.current.style.transform = <span class="hljs-string">`translate3d(<span class="hljs-subst">${xPercentage}</span>%, 0, 0)`</span>;
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    rect.current = item.current.getBoundingClientRect();
  }, [width, height]);

  <span class="hljs-keyword">const</span> loop = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    x.current -= speed.get();
    setX();
  };

  useRafLoop(loop, <span class="hljs-literal">true</span>);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">className</span>=<span class="hljs-string">"item shrink-0 flex gap-[40px] items-center  text-white  whitespace-nowrap"</span>
      <span class="hljs-attr">ref</span>=<span class="hljs-string">{item}</span>
    &gt;</span>
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};
</code></pre>
<p>now in first few lines, i have initialized some variables using <code>useRef</code> hook, so it doesn't cause re-render when we update our variables.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> item = useRef(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">const</span> rect = useRef({});
<span class="hljs-keyword">const</span> x = useRef(<span class="hljs-number">0</span>);
<span class="hljs-keyword">const</span> [width, height] = useWindowSize();
</code></pre>
<p>here, item will be used as a reference to our <code>MarqueeItem</code>div, i am using rect to store the width and height of our item div, here i am defined another <code>x</code> variable in which we will be storing current location of <code>MarqueeItem</code>.<br />we have declared two more variable named width and height, from <code>useWindowSize</code> hook, which i have imported from <code>@react-hook/window-size</code> .</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> setX = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!item.current || !rect.current) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">var</span> xPercentage = (x.current / rect.current.width) * <span class="hljs-number">100</span>;
    <span class="hljs-keyword">if</span> (xPercentage &lt; <span class="hljs-number">-100</span>) x.current = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">if</span> (xPercentage &gt; <span class="hljs-number">0</span>) x.current = -rect.current.width;

    item.current.style.transform = <span class="hljs-string">`translate3d(<span class="hljs-subst">${xPercentage}</span>%, 0, 0)`</span>;
  };
</code></pre>
<p>In the setX function , firstly i am checking if our item ref has any element or not.<br />and in xPercentage variable, i am storing how much we will need to shift our Marquee item in x direction. you can play around with formula which you like, but what i am doing here to calculate is, getting the current location by x.current , diving it by the width of our container and multiplying it by 100,</p>
<p><code>if (xPercentage &lt; -100) x.current = 0;</code> , if the calculated xPercetage is less than -100 , then it has moved out of the screen to the left so update the current location to 0 by doing <code>x.current = 0</code>.</p>
<p><code>if (xPercentage &gt; 0) x.current = -rect.current.width;</code> , If the calculated <code>xPercentage</code> is greater than <code>0</code> , the item has scrolled out of view to the right, <code>x.current</code> is updated to the negative value of the item's width, so it wraps around to the left side.</p>
<p>now we are just updating the MarqueeItem using style transform, we are using translate3d for better performance, as it leverage GPU acceleration.</p>
<pre><code class="lang-javascript">useEffect(<span class="hljs-function">() =&gt;</span> {
    rect.current = item.current.getBoundingClientRect();
  }, [width, height]);
</code></pre>
<p>I have just added a <code>useEffect</code> with dependencies of width and height so whenever screen width, height changes we update the <code>rect.current</code> also.</p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">const</span> loop = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    x.current -= speed.get();
    setX();
  };
  useRafLoop(loop, <span class="hljs-literal">true</span>);
</code></pre>
<p>in loop function , I am subtracting the speed value from <code>x.current</code>, this will update the <code>MarqueeItem</code> container position based on speed. and after updating the <code>x.current</code> we are calling <code>setX</code> function so it reflects the changes. and we are calling it continuously using <code>useRafLoop</code> hook.</p>
<p>now let's create a card component which will have the image and text,</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709876799923/e4b8f219-fea3-49f7-8b35-461ff6dc378e.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> Card = <span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex pointer-events-none   shrink-0 justify-center items-center gap-[30px]"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-h-[104px] rounded-xl relative max-w-[104px] overflow-clip "</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
          <span class="hljs-attr">alt</span>=<span class="hljs-string">{item.title}</span>
          <span class="hljs-attr">src</span>=<span class="hljs-string">{item.image}</span>
          <span class="hljs-attr">className</span>=<span class="hljs-string">"  h-full w-full min-h-[104px] min-w-[104px] object-cover object-center"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-[3rem] font-medium w-full whitespace-nowrap"</span>&gt;</span>
        {item.title}
      <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};
</code></pre>
<p>let's go over it again,</p>
<p>add this to render our <code>MarqueeContainer</code> in our Page</p>
<pre><code class="lang-javascript">&lt;div className=<span class="hljs-string">"mt-[10rem]"</span>&gt;
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeContainr</span>
          <span class="hljs-attr">x</span>=<span class="hljs-string">{x}</span>
          <span class="hljs-attr">direction</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">left</span>"}
          <span class="hljs-attr">springDetails</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">damping:</span> <span class="hljs-attr">200</span>, <span class="hljs-attr">stiffness:</span> <span class="hljs-attr">1000</span>, <span class="hljs-attr">mass:</span> <span class="hljs-attr">1</span> }}
          <span class="hljs-attr">cardData</span>=<span class="hljs-string">{data}</span>
        /&gt;</span></span>
&lt;/div&gt;
</code></pre>
<p>now in our <code>MarqueeContainer</code> , add this , this will render the components four time to dont leave any gap. i have also made this <code>motion.div</code> element , i have imported motion from framer-motion , this is just how framer motion work, whatever element which we want to animate we can just make it a motion element.</p>
<pre><code class="lang-javascript"> &lt;motion.div
        className=<span class="hljs-string">"flex gap-[30px]"</span>
      &gt;
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeItem</span> <span class="hljs-attr">speed</span>=<span class="hljs-string">{speed}</span>&gt;</span>
          {cardData.map((item, index) =&gt; (
            <span class="hljs-tag">&lt;<span class="hljs-name">Card</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span> <span class="hljs-attr">item</span>=<span class="hljs-string">{item}</span> /&gt;</span>
          ))}
        <span class="hljs-tag">&lt;/<span class="hljs-name">MarqueeItem</span>&gt;</span></span>
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeItem</span> <span class="hljs-attr">speed</span>=<span class="hljs-string">{speed}</span>&gt;</span>
          {cardData.map((item, index) =&gt; (
            <span class="hljs-tag">&lt;<span class="hljs-name">Card</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span> <span class="hljs-attr">item</span>=<span class="hljs-string">{item}</span> /&gt;</span>
          ))}
        <span class="hljs-tag">&lt;/<span class="hljs-name">MarqueeItem</span>&gt;</span></span>
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeItem</span> <span class="hljs-attr">speed</span>=<span class="hljs-string">{speed}</span>&gt;</span>
          {cardData.map((item, index) =&gt; (
            <span class="hljs-tag">&lt;<span class="hljs-name">Card</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span> <span class="hljs-attr">item</span>=<span class="hljs-string">{item}</span> /&gt;</span>
          ))}
        <span class="hljs-tag">&lt;/<span class="hljs-name">MarqueeItem</span>&gt;</span></span>{<span class="hljs-string">" "</span>}
        &lt;MarqueeItem speed={speed}&gt;
          {cardData.map(<span class="hljs-function">(<span class="hljs-params">item, index</span>) =&gt;</span> (
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Card</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span> <span class="hljs-attr">item</span>=<span class="hljs-string">{item}</span> /&gt;</span></span>
          ))}
        &lt;/MarqueeItem&gt;
&lt;/motion.div&gt;
</code></pre>
<p>currently it will look like this ,</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709878510138/d5d1851b-1d17-439d-b5ee-5dc310747607.png" alt class="image--center mx-auto" /></p>
<p>to add more rows, we can just multiple <code>MarqueeContainer</code> , but keep in mind, you need to create a new variable using <code>useRef</code> , which will store the speed of that <code>MarqueeContainer</code> in that , if you will pass the current <code>x</code> , then every instance will have the same speed,</p>
<p>here is how to add multiple rows,</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> x = useRef(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> x2 = useRef(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> x3 = useRef(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> x4 = useRef(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> onWheel = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> normalized = normalizeWheel(e);
    x.current = normalized.pixelY * _.wheelFactor;
    x2.current = normalized.pixelY * _.wheelFactor;
    x3.current = normalized.pixelY * _.wheelFactor;
    x4.current = normalized.pixelY * _.wheelFactor;
  };
</code></pre>
<pre><code class="lang-javascript"> &lt;div className=<span class="hljs-string">"mt-[10rem] flex flex-col gap-[30px]"</span>&gt;
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeContainer</span>
          <span class="hljs-attr">x</span>=<span class="hljs-string">{x}</span>
          <span class="hljs-attr">direction</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">left</span>"}
          <span class="hljs-attr">springDetails</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">damping:</span> <span class="hljs-attr">200</span>, <span class="hljs-attr">stiffness:</span> <span class="hljs-attr">1000</span>, <span class="hljs-attr">mass:</span> <span class="hljs-attr">1</span> }}
          <span class="hljs-attr">cardData</span>=<span class="hljs-string">{data}</span>
        /&gt;</span></span>
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeContainer</span>
          <span class="hljs-attr">x</span>=<span class="hljs-string">{x2}</span>
          <span class="hljs-attr">direction</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">right</span>"}
          <span class="hljs-attr">springDetails</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">damping:</span> <span class="hljs-attr">200</span>, <span class="hljs-attr">stiffness:</span> <span class="hljs-attr">1000</span>, <span class="hljs-attr">mass:</span> <span class="hljs-attr">1</span> }}
          <span class="hljs-attr">cardData</span>=<span class="hljs-string">{data2}</span>
        /&gt;</span></span>
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeContainer</span>
          <span class="hljs-attr">x</span>=<span class="hljs-string">{x3}</span>
          <span class="hljs-attr">direction</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">left</span>"}
          <span class="hljs-attr">springDetails</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">damping:</span> <span class="hljs-attr">200</span>, <span class="hljs-attr">stiffness:</span> <span class="hljs-attr">1000</span>, <span class="hljs-attr">mass:</span> <span class="hljs-attr">1</span> }}
          <span class="hljs-attr">cardData</span>=<span class="hljs-string">{data3}</span>
        /&gt;</span></span>
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">MarqueeContainer</span>
          <span class="hljs-attr">x</span>=<span class="hljs-string">{x4}</span>
          <span class="hljs-attr">direction</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">right</span>"}
          <span class="hljs-attr">springDetails</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">damping:</span> <span class="hljs-attr">200</span>, <span class="hljs-attr">stiffness:</span> <span class="hljs-attr">1000</span>, <span class="hljs-attr">mass:</span> <span class="hljs-attr">1</span> }}
          <span class="hljs-attr">cardData</span>=<span class="hljs-string">{data4}</span>
        /&gt;</span></span>
      &lt;/div&gt;
</code></pre>
<p>you can play around with damping and stiffness and mass of each <code>MarqueeContainer</code> instance so every row will have some other effects.</p>
<p>I have added some more effects like dragging and moving , slowdown , and a lot more , so you can checkout the github repo if you are interested in adding some more effects.<br />Here is the Github Repo :- <a target="_blank" href="https://github.com/notpritam/park">notpritam/park (github.com)</a><br />If you have any questions feel free to comment...</p>
]]></content:encoded></item></channel></rss>