Creating a Dynamic Product Carousel Using Section Blocks

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

A well-designed product carousel can instantly capture shopper attention, showcase featured items, and boost conversions on your Shopify store. With Shopify 2.0’s flexible section blocks, you can build a truly dynamic, merchant-friendly carousel that lets store owners add, reorder, and customize slides right from the theme editor—no code required. In this guide, you’ll learn how to scaffold a carousel section, define blocks for each slide, style with CSS for smooth transitions, and add JavaScript for swipe and autoplay behavior. By the end, you’ll have a reusable, responsive product carousel that adapts to any storefront and drives engagement.

Understanding Section Blocks for Carousels

What Are Section Blocks?

Shopify 2.0 enables sections on every page and introduces blocks—repeatable, draggable items within a section. Blocks are ideal for carousels because each slide can be a block with its own settings (image, heading, link).

  • Dynamic Management: Merchants can add or remove slides in the theme editor.
  • Custom Settings per Slide: Unique images, headlines, buttons per block.
  • Reorder Easily: Drag-and-drop to change slide order without touching code.
  • Preset Configurations: Provide default slides via presets so the carousel looks ready out of the box.

Prerequisites and Setup

  1. Shopify 2.0 Theme: Ensure your theme supports JSON templates and sections.
  2. Shopify CLI: For local development (shopify theme serve).
  3. Code Editor: VS Code or similar, with Liquid support.
  4. Basic JavaScript & CSS: Familiarity with event listeners and transforms.

Initialize your theme locally:

bashCopyEditshopify login --store your-dev-store.myshopify.com
shopify theme init my-carousel-theme
cd my-carousel-theme
shopify theme serve

Create a new file at sections/product-carousel.liquid. Begin with the boilerplate:

liquidCopyEdit{% schema %}
{
  "name": "Product Carousel",
  "max_blocks": 8,
  "blocks": [
    {
      "type": "slide",
      "name": "Slide",
      "settings": [
        {
          "type": "product",
          "id": "product",
          "label": "Select Product"
        },
        {
          "type": "image_picker",
          "id": "image",
          "label": "Custom Slide Image (optional)"
        },
        {
          "type": "text",
          "id": "heading",
          "label": "Slide Heading",
          "default": ""
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Default Carousel",
      "category": "Featured"
    }
  ]
}
{% endschema %}
  • max_blocks limits slides to 8.
  • blocks array defines each slide’s settings: picking a product, optional override image, and heading.
  • presets seeds a default empty carousel in new themes.

Above the schema, add markup that loops through each block:

liquidCopyEdit<div class="carousel" data-carousel>
  <div class="carousel__track">
    {% for block in section.blocks %}
      {% assign prod = all_products[block.settings.product] %}
      <div class="carousel__slide" data-slide-index="{{ forloop.index0 }}">
        <a href="{{ prod.url }}" class="carousel__link">
          <img
            src="{{ block.settings.image | img_url: '800x' | default: prod.featured_image | img_url: '800x' }}"
            alt="{{ block.settings.heading | default: prod.title }}"
            class="carousel__image"
          >
        </a>
        <div class="carousel__caption">
          <h3>{{ block.settings.heading | default: prod.title }}</h3>
          <p>{{ prod.price | money }}</p>
        </div>
      </div>
    {% endfor %}
  </div>
  <button class="carousel__prev" aria-label="Previous slide">‹</button>
  <button class="carousel__next" aria-label="Next slide">›</button>
</div>
  • .carousel__track: Flex container for slides.
  • data- attributes: Used to initialize JS behavior.
  • Image source: Uses custom image if set, else product’s featured image.
  • Caption: Heading and price pulled dynamically.

Step 3: Styling with CSS

Add styles in assets/product-carousel.css:

cssCopyEdit.carousel {
  position: relative;
  overflow: hidden;
}
.carousel__track {
  display: flex;
  transition: transform 0.5s ease;
}
.carousel__slide {
  min-width: 100%;
  box-sizing: border-box;
  position: relative;
}
.carousel__image {
  width: 100%;
  display: block;
}
.carousel__caption {
  position: absolute;
  bottom: 1rem;
  left: 1rem;
  background: rgba(0,0,0,0.5);
  color: #fff;
  padding: 0.5rem 1rem;
  border-radius: 4px;
}
.carousel__prev,
.carousel__next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0,0,0,0.5);
  border: none;
  color: #fff;
  font-size: 2rem;
  padding: 0 0.5rem;
  cursor: pointer;
}
.carousel__prev { left: 0.5rem; }
.carousel__next { right: 0.5rem; }

Include the stylesheet in your layout:

liquidCopyEdit{{ 'product-carousel.css' | asset_url | stylesheet_tag }}

Step 4: Adding JavaScript for Interactivity

Create assets/product-carousel.js:

jsCopyEditdocument.addEventListener('DOMContentLoaded', () => {
  const carousels = document.querySelectorAll('[data-carousel]');
  carousels.forEach(initCarousel);
});

function initCarousel(carousel) {
  const track = carousel.querySelector('.carousel__track');
  const slides = Array.from(track.children);
  const prevButton = carousel.querySelector('.carousel__prev');
  const nextButton = carousel.querySelector('.carousel__next');
  let currentIndex = 0;

  function updateTrack() {
    const offset = -currentIndex * carousel.clientWidth;
    track.style.transform = `translateX(${offset}px)`;
  }

  prevButton.addEventListener('click', () => {
    currentIndex = (currentIndex - 1 + slides.length) % slides.length;
    updateTrack();
  });
  nextButton.addEventListener('click', () => {
    currentIndex = (currentIndex + 1) % slides.length;
    updateTrack();
  });

  // Optional autoplay
  let autoplay = carousel.dataset.autoplay === 'true';
  if (autoplay) {
    setInterval(() => {
      nextButton.click();
    }, parseInt(carousel.dataset.autoplayInterval, 10) || 5000);
  }

  // Resize handler
  window.addEventListener('resize', updateTrack);
}

Include it in your layout under <body>:

liquidCopyEdit{{ 'product-carousel.js' | asset_url | script_tag }}
  • updateTrack() shifts slides via CSS transforms.
  • Autoplay support via data-autoplay and data-autoplay-interval attributes.
  • Responsive: Recalculates positions on resize.

Step 5: Exposing Autoplay Settings (Optional)

Allow merchants to toggle autoplay and interval via section settings. In your schema’s root:

jsonCopyEdit"settings": [
  {
    "type": "checkbox",
    "id": "autoplay",
    "label": "Enable Autoplay",
    "default": true
  },
  {
    "type": "number",
    "id": "autoplay_interval",
    "label": "Autoplay Interval (ms)",
    "min": 2000,
    "max": 10000,
    "step": 500,
    "default": 5000
  }
]

In the markup wrapper:

liquidCopyEdit<div
  class="carousel"
  data-carousel
  data-autoplay="{{ section.settings.autoplay }}"
  data-autoplay-interval="{{ section.settings.autoplay_interval }}"
>

Merchants can now control autoplay behavior directly in the theme editor.

Conclusion

Building a dynamic product carousel using Shopify 2.0 section blocks combines the power of Liquid, CSS, and JavaScript to create a flexible, user-friendly component. By defining block-based slides, crafting responsive styles, and wiring up interactive behavior, you give merchants a drag-and-drop carousel they can manage without code. Layer in optional autoplay settings and thoughtful defaults via presets, and you’ll deliver a polished, conversion-driving carousel that enhances any storefront. Ready to bring your products into motion? Start customizing and watch engagement soar.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts