Shopify Section Scroll Reveal Effect (Copy-Paste Example, No Apps)

Shopify Section Scroll Reveal Effect (Copy-Paste Example, No Apps)

Shopify Section Scroll Reveal Effect
Shopify Section Scroll Reveal Effect

In this post you’ll build a Shopify section scroll-reveal effect with pure HTML/CSS/JS. We’ll use IntersectionObserver to watch when elements become visible and then apply small transitions. The snippet is just an example — copy/paste it into a Custom HTML (or Custom Liquid) block, customize colors/timings, and reuse it on any page. If you want theme settings later, you can port the same markup into a Liquid section.

What you’ll get:

  • Multiple effect presets: fade-upslide-left/rightscale-in
  • Stagger per child for satisfying cascades
  • Reduced-motion safe (no animation for users who prefer less motion)
  • One small script, no dependencies, no apps

Where to paste this in Shopify

  1. Online Store → Themes → Customize
  2. Open the template (Home, Collection, Blog, etc.)
  3. Add section → Custom HTML (or Custom Liquid)
  4. Paste the entire snippet below
  5. Save and preview on desktop + mobile

Copy-Paste Example — Section Scroll Reveal

Paste everything from <!DOCTYPE html> to </html> into your Custom HTML/Liquid block.
Remember: this is an example; change content, colors, and timings to match your brand.

DESIGNED FOR REFERENCE ONLY
<body>


  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Shopify Section Scroll Reveal Effect</title>
  <style>
    /* ===== Brand tokens (tweak freely) ===== */
    :root{
      --bg: #0f1115;
      --fg: #e8ecf1;
      --muted: #aeb8c6;
      --accent: #00d084;
      --card: #151924;
      --radius: 16px;
      --gap: 22px;
      --pad: 20px;
      --shadow: 0 14px 40px rgba(0,0,0,.28);
    }
    html, body { margin:0; background:var(--bg); color:var(--fg); font-family: Arial, sans-serif; }
    .container { max-width: 1200px; margin: 0 auto; padding: 38px var(--pad) 90px; }

    /* ===== Demo sections ===== */
    .section {
      margin: 40px 0;
      background: var(--card);
      border-radius: var(--radius);
      border: 1px solid #1f2636;
      box-shadow: var(--shadow);
      overflow: hidden;
    }
    .section-head {
      display:flex; justify-content:space-between; align-items:center; gap:10px;
      padding: 18px var(--pad);
      background: #13192a;
      border-bottom: 1px solid #1f2636;
    }
    .section-title { font-weight: 800; letter-spacing: .2px; }
    .section-body {
      padding: 22px var(--pad) 26px;
      display:grid;
      grid-template-columns: repeat(3, 1fr);
      gap: var(--gap);
    }
    .card {
      background: #101625;
      border: 1px solid #1c2334;
      border-radius: 14px;
      overflow: hidden;
    }
    .card img { width:100%; height:180px; object-fit:cover; display:block; }
    .card .pad { padding: 12px; }
    .card h3 { margin: 6px 0 4px; font-size: 16px; }
    .card p  { margin: 0; font-size: 13px; color: var(--muted); }
    @media (max-width: 980px){ .section-body { grid-template-columns: repeat(2, 1fr); } }
    @media (max-width: 640px){ .section-body { grid-template-columns: 1fr; } }

    /* ===== Scroll-Reveal: base states =====
       Apply [data-reveal] to any element you want to animate in.
       JS adds .is-in when it intersects the viewport. */
    [data-reveal]{
      opacity: 0;
      transform: translate3d(0, 14px, 0) scale(0.98);
      filter: none;
      will-change: opacity, transform, filter;
      transition-property: opacity, transform, filter;
      transition-duration: 640ms;
      transition-timing-function: cubic-bezier(.2,.65,.2,1);
      transition-delay: 0ms;
    }
    .is-in{
      opacity: 1;
      transform: none;
      filter: none;
    }

    /* ===== Presets (optional; combine with [data-reveal]) ===== */
    .rv-fade-up   { transform: translate3d(0, 18px, 0); }
    .rv-slide-l   { transform: translate3d(26px, 0, 0); }
    .rv-slide-r   { transform: translate3d(-26px, 0, 0); }
    .rv-scale-in  { transform: scale(.92); }
    .rv-blur-in   { filter: blur(6px); }

    /* Respect reduced motion */
    @media (prefers-reduced-motion: reduce){
      [data-reveal]{
        transition-duration: 1ms !important;
        transform: none !important;
        filter: none !important;
      }
    }
  </style>


  <div class="container">
    <!-- SECTION 1 -->
    <section class="section" data-reveal data-stagger=".card" data-stagger-step="80">
      <div class="section-head">
        <h2 class="section-title">Featured Products</h2>
        <a href="/collections/all" style="color:var(--accent); text-decoration:none; font-weight:800">View all →</a>
      </div>
      <div class="section-body">
        <article class="card rv-fade-up" data-reveal>
          <img src="https://picsum.photos/id/1040/800/600" alt="Item 1">
          <div class="pad">
<h3>Merino Crew</h3>
<p>All-season knit</p>
</div>
        </article>
        <article class="card rv-fade-up" data-reveal>
          <img src="https://picsum.photos/id/1069/800/600" alt="Item 2">
          <div class="pad">
<h3>Packable Puffer</h3>
<p>Warmth, zero bulk</p>
</div>
        </article>
        <article class="card rv-fade-up" data-reveal>
          <img src="https://picsum.photos/id/1011/800/600" alt="Item 3">
          <div class="pad">
<h3>Minimal Runner</h3>
<p>Everyday comfort</p>
</div>
        </article>
      </div>
    </section>

    <!-- SECTION 2 -->
    <section class="section" data-reveal data-offset="0.2">
      <div class="section-head">
        <h2 class="section-title">Why Shop With Us</h2>
      </div>
      <div class="section-body">
        <article class="card rv-slide-l" data-reveal>
          <img src="https://picsum.photos/id/1003/800/600" alt="Quality">
          <div class="pad">
<h3>Premium Quality</h3>
<p>Materials that last</p>
</div>
        </article>
        <article class="card rv-scale-in" data-reveal>
          <img src="https://picsum.photos/id/1015/800/600" alt="Shipping">
          <div class="pad">
<h3>Fast Shipping</h3>
<p>Free over $50</p>
</div>
        </article>
        <article class="card rv-slide-r" data-reveal>
          <img src="https://picsum.photos/id/1025/800/600" alt="Support">
          <div class="pad">
<h3>5-Star Support</h3>
<p>We’ve got your back</p>
</div>
        </article>
      </div>
    </section>

    <!-- SECTION 3 -->
    <section class="section" data-reveal data-stagger=".feature" data-stagger-step="90" data-repeat="true">
      <div class="section-head">
        <h2 class="section-title">Highlights</h2>
      </div>
      <div class="section-body">
        <div class="card feature rv-fade-up" data-reveal>
          <div class="pad">
<h3>Sustainable</h3>
<p>Ethical sourcing & packaging</p>
</div>
        </div>
        <div class="card feature rv-fade-up" data-reveal>
          <div class="pad">
<h3>Easy Returns</h3>
<p>30-day hassle-free</p>
</div>
        </div>
        <div class="card feature rv-fade-up" data-reveal>
          <div class="pad">
<h3>Rewards</h3>
<p>Earn as you shop</p>
</div>
        </div>
      </div>
    </section>
  </div>

  <script>
    (function(){
      const prefersReduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      if (prefersReduced || !('IntersectionObserver' in window)) {
        document.querySelectorAll('[data-reveal]').forEach(el => el.classList.add('is-in'));
        return;
      }

      // Helper: apply stagger to children
      function applyStagger(container){
        const sel = container.getAttribute('data-stagger');
        if(!sel) return;
        const step = parseInt(container.getAttribute('data-stagger-step') || '80', 10);
        const kids = container.querySelectorAll(sel + '[data-reveal], ' + sel + ' [data-reveal]');
        kids.forEach((el, i) => {
          el.style.transitionDelay = (i * step) + 'ms';
        });
      }

      // Prepare containers with stagger
      document.querySelectorAll('.section[data-reveal]')</script>
</body>

How it works (quick)

  • HTML: Add data-reveal to any section or card you want to animate. For staggered cascades, put data-stagger=".childSelector" on the section and each child you want to reveal should also have data-reveal.
  • CSS: Elements start slightly offset/invisible. When .is-in is applied, they transition to their natural state. Preset classes (rv-fade-up, rv-slide-l, rv-slide-r, rv-scale-in, rv-blur-in) tweak the starting transform.
  • JS: A single IntersectionObserver toggles .is-in when elements enter/leave the viewport. Optional attributes:data-stagger + data-stagger-step="80" — per-child delaysdata-offset="0.2" — custom threshold (20% visible)data-repeat="true" — re-animate when re-entering view
  • Accessibility: Honors prefers-reduced-motion — content shows instantly without animation.

Customization quick hits

  • Change effect per element by swapping preset classes.
  • Global timing: edit transition-duration in [data-reveal].
  • Per-item timing: add inline style transition-duration: 700ms or set via JS if you prefer.
  • Intensity: increase/decrease the starting transform in the preset rules.
  • Mobile: keep transforms small and durations short for snappier feel.

Pro tips

  • Animate sections first (titles/headers), then cards inside with a short stagger (60–120ms).
  • Use motion to clarify hierarchy; don’t animate everything equally.
  • Balance performance: transforms + opacity are GPU-friendly; avoid animating layout properties.
  • If your theme already uses reveal effects, avoid double-animating the same elements.

Final note on “Liquid”

This tutorial uses pure HTML/CSS/JS so anyone can paste and learn quickly. If you want the same effect as a Liquid section with editor settings (image pickers, text fields, toggles), you can wrap this markup into /sections/your-section.liquid and expose settings via schema — the reveal classes and script stay the same.