Beschikbaar voor nieuwe Shopify-projecten

Conversie & CROTutorial

Social proof op je productpagina, zonder app: vier snippets

Diek ThunnissenDiek Thunnissen8 sep 202614 min leestijd

Een kijkers-teller, een verkoop-notificatie, een testimonials-slider en video-testimonials, als losse snippets in je eigen thema. Met de complete code, en met de kanttekening wanneer social proof nep voelt en je juist conversie kost.

Mensen kopen wat andere mensen kopen. Dat is geen marketingtruc, dat is hoe we beslissen als we iets niet zeker weten. Een productpagina die laat zien dat er al klanten zijn, verkoopt makkelijker dan een pagina die alleen zichzelf aanprijst.

De meeste shops halen dat uit een app. Een abonnement per maand, een script op elke pagina, en getallen waarvan niemand weet waar ze vandaan komen. De vier onderdelen hieronder hebben we daarom als snippet gebouwd. Je plakt ze in een custom liquid-blok of een eigen sectie en je bent er zelf de baas over.

Eén ding vooraf. Social proof werkt alleen als het klopt. Zodra een bezoeker het gevoel krijgt dat de getallen verzonnen zijn, keert het effect om en kost het je vertrouwen. Onderaan komen we daar op terug, want twee van de vier snippets vragen daar om.

1. De kijkers-teller

Een klein blok onder de producttitel: een pulserend bolletje en de tekst dat 54 mensen dit product nu bekijken. Bij elke refresh en elke klik verandert het getal. Het is een custom liquid-blok in de theme editor. Sleep hem onder de titel, naast de prijs of boven de winkelwagenknop.

148 regels codeToon code +
<div class="watching-box">
  <div class="watching-icon">
    <div class="pulse-container">
      <div class="circle">
        <div class="inner-circle"></div>
      </div>
      <div class="pulse"></div>
    </div>
  </div>
  <span class="watching-text">
    <strong><span id="viewer-count-{{ product.id }}">54</span> mensen</strong> bekijken dit product nu
  </span>
</div>
<style>
  /* Widget Styling */
  .watching-box {
    display: flex;
    align-items: center;
    background: linear-gradient(to right, #ffffff, #f9fcff);
    padding: 8px 16px;
    border-radius: 20px;
    box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.08);
    font-family: 'Helvetica Neue', Arial, sans-serif;
    font-size: 14px;
    color: #333;
    font-weight: 400;
    max-width: fit-content;
    margin: 15px 0;
    border: 1px solid rgba(141, 195, 230, 0.3);
    transition: transform 0.2s ease;
  }
  
  .watching-box:hover {
    transform: translateY(-2px);
    box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.12);
  }
  /* Icon Styling */
  .watching-icon {
    display: flex;
    align-items: center;
    margin-right: 10px;
  }
  
  .pulse-container {
    position: relative;
  }
  .circle {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 2px solid #8dc3e6;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    z-index: 2;
    background-color: white;
  }
  .inner-circle {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #8dc3e6;
  }
  
  .pulse {
    position: absolute;
    top: -2px;
    left: -2px;
    right: -2px;
    bottom: -2px;
    border-radius: 50%;
    background: rgba(141, 195, 230, 0.4);
    z-index: 1;
    animation: pulse 2s infinite;
  }
  /* Text Styling */
  .watching-text {
    font-size: 14px;
    color: #444;
    letter-spacing: 0.2px;
  }
  .watching-text strong {
    font-weight: 600;
    color: #222;
  }
  
  @keyframes pulse {
    0% {
      transform: scale(1);
      opacity: 0.8;
    }
    70% {
      transform: scale(1.5);
      opacity: 0;
    }
    100% {
      transform: scale(1.5);
      opacity: 0;
    }
  }
</style>
<script>
  // Wacht tot de pagina is geladen
  document.addEventListener('DOMContentLoaded', function() {
    // Gebruik product ID om unieke element ID te maken
    const viewerCountId = "viewer-count-{{ product.id }}";
    const countElement = document.getElementById(viewerCountId);
    
    // Functie om een willekeurig getal te genereren
    function getRandomNumber(min, max) {
      return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    
    // Functie om het aantal kijkers bij te werken
    function updateViewerCount() {
      // Genereer een willekeurig getal tussen 45 en 98
      const randomViewers = getRandomNumber(45, 98);
      
      // Voeg een visueel effect toe bij het veranderen
      countElement.style.transition = 'color 0.3s ease';
      countElement.style.color = '#007bff';
      
      // Update het aantal kijkers
      countElement.textContent = randomViewers;
      
      // Keer terug naar de normale kleur na de transitie
      setTimeout(() => {
        countElement.style.color = '#222';
      }, 500);
      
      // Sla het nieuwe aantal op in localStorage
      localStorage.setItem(viewerCountId, randomViewers);
    }
    
    // Controleer of er een opgeslagen waarde is
    const savedCount = localStorage.getItem(viewerCountId);
    if (savedCount) {
      countElement.textContent = savedCount;
    } else {
      updateViewerCount();
    }

    // Aanvulling DIEK, sep 2026: de oorspronkelijke post brak hier af.
    // Bij een klik op het blok verandert het getal opnieuw.
    document.querySelector('.watching-box').addEventListener('click', updateViewerCount);
  });
</script>

De oorspronkelijke post brak halverwege het script af. De laatste regels, vanaf de opgeslagen waarde en de klik, hebben we aangevuld en als zodanig gemarkeerd in de code.

Aanpassen: de blauwe kleur van de cirkel zit in #8dc3e6, de achtergrond in de linear-gradient, en de bandbreedte van het getal in getRandomNumber(45, 98). De tekst pas je aan in de HTML. Wil je het minimum per product sturen, lees het dan uit een metafield in plaats van een vast getal.

{{ product.metafields.custom.min_viewers | default: 54 }}

Een variant koppelt de urgentie aan de voorraad, zodat het blok een extra klasse krijgt zodra er minder dan tien stuks zijn. Je hangt daar dan je eigen styling aan.

{% if product.inventory_quantity < 10 %}
  <div class="watching-box urgent">
{% else %}
  <div class="watching-box">
{% endif %}

Let wel: dit getal is willekeurig. Dat staat letterlijk in de code. Wat je hier bouwt is een visueel element, geen meting.

2. De verkoop-notificatie

Een compact kaartje met een avatar-icoon, de zin dat Emma uit Amsterdam dit product heeft gekocht, en daaronder het woord zojuist. Ook een custom liquid-blok. Op de productpagina onder de titel of boven de winkelwagenknop, op een collectiepagina bovenaan.

131 regels codeToon code +
<!-- Shopify Sales Notification - Clean, Professional Design -->
<style>
  .sales-popup {
    display: flex;
    align-items: center;
    background-color: #ffffff;
    color: #1a1a1a;
    padding: 10px 14px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 400;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
    width: fit-content;
    margin: 0 auto;
    border: 1px solid #f0f0f0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    line-height: 1.4;
    transition: opacity 0.3s ease;
    max-width: 300px;
    position: relative;
  }
  
  .sales-popup-avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background-color: #f3f4f6;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 10px;
    flex-shrink: 0;
  }
  
  .sales-popup-avatar svg {
    width: 16px;
    height: 16px;
    fill: #5c6ac4;
  }
  
  .sales-popup-content {
    flex-grow: 1;
    overflow: hidden;
  }
  
  .sales-popup-message {
    margin: 0;
    white-space: normal;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  
  .sales-popup-name {
    font-weight: 600;
    color: #212b36;
  }
  
  .sales-popup-time {
    font-size: 12px;
    color: #637381;
    margin-top: 2px;
  }
  
  /* Verwijder de sluitknop en hover effecten */
  .sales-popup-close {
    display: none;
  }
  
  @media (max-width: 768px) {
    .sales-popup {
      max-width: 280px;
      margin-left: 10px;
      margin-right: 10px;
    }
  }
</style>
<div class="sales-popup">
  <div class="sales-popup-avatar">
    <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
      <path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" />
    </svg>
  </div>
  
  <div class="sales-popup-content">
    <p class="sales-popup-message">
      <span id="dutch-name" class="sales-popup-name">Emma</span> uit <span id="dutch-city">Amsterdam</span> heeft dit product gekocht
    </p>
    <div class="sales-popup-time">zojuist</div>
  </div>
</div>
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Lijst van populaire Nederlandse voornamen
    const dutchNames = [
      "Emma", "Noah", "Julia", "Sem", "Sophie", "Lucas", "Lieke", "Finn", 
      "Mila", "Daan", "Tess", "Tim", "Sara", "Thomas", "Lisa", "Milan", 
      "Anna", "Luuk", "Eva", "Jesse", "Noor", "Bram", "Evi", "Max", 
      "Nina", "Thijs", "Zoë", "Sam", "Fleur", "Sven", "Lotte", "Stijn"
    ];
    
    // Lijst van Nederlandse steden/plaatsen
    const dutchCities = [
      "Amsterdam", "Rotterdam", "Utrecht", "Den Haag", "Eindhoven", 
      "Groningen", "Tilburg", "Breda", "Nijmegen", "Enschede", 
      "Haarlem", "Arnhem", "Amersfoort", "Zaanstad", "Den Bosch", 
      "Zwolle", "Maastricht", "Leiden", "Dordrecht", "Alkmaar"
    ];
    
    // Functie om een willekeurig element uit een array te selecteren
    function getRandomElement(array) {
      return array[Math.floor(Math.random() * array.length)];
    }
    
    const nameElement = document.getElementById('dutch-name');
    const cityElement = document.getElementById('dutch-city');
    
    // Stel een vaste naam en stad in bij het laden
    nameElement.textContent = getRandomElement(dutchNames);
    cityElement.textContent = getRandomElement(dutchCities);
    
    // Toon de notificatie permanent
    document.querySelector('.sales-popup').style.display = 'flex';
    document.querySelector('.sales-popup').style.opacity = '1';
    
    // Verwijder de sluitknop omdat we deze niet meer nodig hebben
    const closeButton = document.querySelector('.sales-popup-close');
    if (closeButton) {
      closeButton.remove();
    }
  });
</script>

Aanpassen: achtergrondkleur, randkleur en avatarkleur staan in de CSS (background-color, border en fill). De zin staat in de HTML, bijvoorbeeld “heeft zojuist gekocht bij” gevolgd door je merknaam. De lijsten met namen en steden staan in het JavaScript en mag je uitbreiden.

Ook hier worden naam en stad willekeurig gekozen. De oude post beloofde een bonus om dit aan echte orders te koppelen via shop.orders in Liquid, maar dat object bestaat niet in een thema. Echte data betekent een app proxy, of een Flow die het laatste bestelmoment in een shop-metafield schrijft. Dat is een apart artikel.

3. De testimonials-slider

Een eigen sectie met blokken. Per testimonial een foto, een auteursfoto, een naam, sterren en tekst. De kaarten staan in een slider met pijlen, autoplay, swipe op mobiel en oneindig doorlopen. Aantal slides per schermbreedte, kleuren, radius en afstanden stel je in de theme editor in.

Maak in de code-editor onder Sections een nieuwe sectie, bijvoorbeeld testimonials-slider, en plak dit erin. Daarna voeg je DT Testimonials Slider toe via Sectie toevoegen en vul je de testimonials in.

656 regels codeToon code +
<!-- Voeg dit toe aan je theme.liquid in de head sectie:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
-->
<div class="testimonials-section" id="testimonials-{{ section.id }}">
  <div class="page-width">
    <div class="testimonials-slider">
      <div class="slider-track">
        {% for block in section.blocks %}
          <div class="slide" {{ block.shopify_attributes }}>
            <div class="testimonial-card">
              {% if block.settings.image %}
                <div class="testimonial-image">
                  {{ block.settings.image | image_url: width: 1000 | image_tag: loading: 'lazy', class: 'card-image' }}
                </div>
              {% endif %}
              
              <div class="testimonial-content">
                <div class="author-info">
                  {% if block.settings.author_image %}
                    {{ block.settings.author_image | image_url: width: 250 | image_tag: loading: 'lazy', class: 'author-image' }}
                  {% endif %}
                  <div class="author-details">
                    <h4>{{ block.settings.author }}</h4>
                  </div>
                </div>
                <div class="rating">
                  {% for i in (1..block.settings.rating) %}
                    <span class="star">★</span>
                  {% endfor %}
                </div>
                <div class="testimonial-text">
                  {{ block.settings.text }}
                </div>
              </div>
            </div>
          </div>
        {% endfor %}
      </div>
      <button class="slider-nav prev" aria-label="Previous">❮</button>
      <button class="slider-nav next" aria-label="Next">❯</button>
    </div>
  </div>
</div>
<style>
  #testimonials-{{ section.id }} {
    --section-background: {{ section.settings.background_color }};
    --card-background: {{ section.settings.card_background }};
    --text-color: {{ section.settings.text_color }};
    --star-color: {{ section.settings.star_color }};
    --nav-button-color: {{ section.settings.nav_button_color }};
    --card-radius: {{ section.settings.card_radius }}px;
    --image-height: {{ section.settings.image_height }}px;
    --section-padding-top: {{ section.settings.padding_top }}px;
    --section-padding-bottom: {{ section.settings.padding_bottom }}px;
    --space-between: {{ section.settings.space_between }}px;
  }
  .testimonials-section {
    padding: var(--section-padding-top) 0 var(--section-padding-bottom);
    position: relative;
    background: var(--section-background);
  }
  .page-width {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
  }
  .testimonials-slider {
    position: relative;
    overflow: hidden;
    padding: 20px 40px;
  }
  .slider-track {
    display: flex;
    transition: transform 0.5s ease;
    touch-action: pan-y pinch-zoom;
    will-change: transform;
    gap: var(--space-between);
  }
  .slide {
    flex: 0 0 100%;
    padding: 0 10px;
    box-sizing: border-box;
  }
  .testimonial-card {
    border-radius: var(--card-radius);
    border: 1px solid #d9d9d9;
    overflow: hidden;
    height: 100%;
    background: var(--card-background);
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    color: var(--text-color);
  }
  .testimonial-image img {
    width: 100%;
    height: var(--image-height);
    object-fit: cover;
  }
  .testimonial-content {
    padding: 20px;
    text-align: {{ section.settings.text_alignment }};
  }
  .author-info {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 15px;
  }
  .author-image {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
  }
  .rating {
    color: var(--star-color);
    margin: 10px 0;
  }
  .star {
    font-size: 14px;
  }
  .slider-nav {
    {% unless section.settings.show_arrows %}
      display: none;
    {% endunless %}
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background: #fff;
    border: 2px solid var(--nav-button-color);
    color: var(--nav-button-color);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    z-index: 2;
    transition: all 0.3s ease;
  }
  .slider-nav:hover {
    background: var(--nav-button-color);
    color: #fff;
  }
  .slider-nav.prev {
    left: 0;
  }
  .slider-nav.next {
    right: 0;
  }
  @media screen and (min-width: 1024px) {
    .slide {
      flex: 0 0 calc((100% - ({{ section.settings.desktop_slides | minus: 1 }} * var(--space-between))) / {{ section.settings.desktop_slides }});
    }
  }
  @media screen and (min-width: 768px) and (max-width: 1023px) {
    .slide {
      flex: 0 0 calc((100% - ({{ section.settings.tablet_slides | minus: 1 }} * var(--space-between))) / {{ section.settings.tablet_slides }});
    }
  }
  @media screen and (max-width: 767px) {
    .slide {
      flex: 0 0 calc((100% - ({{ section.settings.mobile_slides | minus: 1 }} * var(--space-between))) / {{ section.settings.mobile_slides }});
    }
  }
  @media (hover: none) and (pointer: coarse) {
    .testimonials-slider {
      overflow-x: hidden;
      -webkit-overflow-scrolling: touch;
    }
  }
</style>
<script>
  (function() {
    class TestimonialSlider {
      constructor(container) {
        this.container = container;
        this.track = container.querySelector('.slider-track');
        this.slides = container.querySelectorAll('.slide');
        this.prevButton = container.querySelector('.slider-nav.prev');
        this.nextButton = container.querySelector('.slider-nav.next');
        this.currentIndex = 0;
        this.slideCount = this.slides.length;
        this.interval = null;
        this.isAnimating = false;
        this.touchStartX = 0;
        this.touchEndX = 0;
        this.minSwipeDistance = 50;
        this.animationDuration = 500;
        this.autoplayDelay = 6000;
        if (this.slideCount < 3) return;
        this.setupInfiniteScroll();
        this.init();
      }
      setupInfiniteScroll() {
        const slidesToClone = 3;
        
        for (let i = 0; i < slidesToClone; i++) {
          const clone = this.slides[i].cloneNode(true);
          this.track.appendChild(clone);
        }
        
        for (let i = this.slideCount - 1; i >= this.slideCount - slidesToClone; i--) {
          const clone = this.slides[i].cloneNode(true);
          this.track.insertBefore(clone, this.track.firstChild);
        }
        this.slides = this.container.querySelectorAll('.slide');
        this.currentIndex = slidesToClone;
        requestAnimationFrame(() => this.updateSlider(false));
      }
      init() {
        if (this.slideCount > 0) {
          this.addEventListeners();
          this.startAutoplay();
        }
      }
      updateSlider(animate = true) {
        if (!this.slides[0]) return;
        
        const slideWidth = this.slides[0].offsetWidth;
        
        if (animate) {
          this.track.style.transition = `transform ${this.animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
        } else {
          this.track.style.transition = 'none';
        }
        
        this.track.style.transform = `translateX(-${this.currentIndex * slideWidth}px)`;
      }
      moveToSlide(index, animate = true) {
        if (this.isAnimating || !this.slides[index]) return;
        
        this.isAnimating = true;
        this.currentIndex = index;
        this.updateSlider(animate);
        if (animate) {
          this.scheduleAnimationReset();
        } else {
          this.isAnimating = false;
        }
      }
      scheduleAnimationReset() {
        setTimeout(() => {
          this.isAnimating = false;
        }, this.animationDuration + 50);
      }
      nextSlide() {
        if (this.isAnimating) return;
        
        this.moveToSlide(this.currentIndex + 1, true);
        if (this.currentIndex >= this.slides.length - 3) {
          this.scheduleReset(2);
        }
      }
      prevSlide() {
        if (this.isAnimating) return;
        
        this.moveToSlide(this.currentIndex - 1, true);
        if (this.currentIndex <= 2) {
          this.scheduleReset(this.slides.length - 4);
        }
      }
      scheduleReset(resetIndex) {
        setTimeout(() => {
          requestAnimationFrame(() => {
            this.moveToSlide(resetIndex, false);
          });
        }, this.animationDuration);
      }
      startAutoplay() {
        this.stopAutoplay();
        this.interval = setInterval(() => {
          if (!this.isAnimating && document.visibilityState === 'visible') {
            this.nextSlide();
          }
        }, this.autoplayDelay);
      }
      stopAutoplay() {
        if (this.interval) {
          clearInterval(this.interval);
          this.interval = null;
        }
      }
      addEventListeners() {
        this.nextButton.addEventListener('click', () => {
          if (!this.isAnimating) {
            this.nextSlide();
            this.stopAutoplay();
            this.startAutoplay();
          }
        });
        this.prevButton.addEventListener('click', () => {
          if (!this.isAnimating) {
            this.prevSlide();
            this.stopAutoplay();
            this.startAutoplay();
          }
        });
        let touchMoved = false;
        
        this.track.addEventListener('touchstart', (e) => {
          touchMoved = false;
          this.touchStartX = e.touches[0].clientX;
          this.stopAutoplay();
          this.track.style.transition = 'none';
        }, { passive: true });
        this.track.addEventListener('touchmove', (e) => {
          if (this.isAnimating) return;
          touchMoved = true;
          
          const currentTouch = e.touches[0].clientX;
          const diff = this.touchStartX - currentTouch;
          const slideWidth = this.slides[0].offsetWidth;
          
          requestAnimationFrame(() => {
            const newPosition = -(this.currentIndex * slideWidth) - diff;
            this.track.style.transform = `translateX(${newPosition}px)`;
          });
        }, { passive: true });
        this.track.addEventListener('touchend', (e) => {
          if (!touchMoved) return;
          
          this.touchEndX = e.changedTouches[0].clientX;
          const swipeDistance = this.touchStartX - this.touchEndX;
          
          this.track.style.transition = `transform ${this.animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
          if (Math.abs(swipeDistance) > this.minSwipeDistance) {
            if (swipeDistance > 0) {
              this.nextSlide();
            } else {
              this.prevSlide();
            }
          } else {
            this.updateSlider(true);
          }
          setTimeout(() => this.startAutoplay(), this.animationDuration);
        }, { passive: true });
        document.addEventListener('visibilitychange', () => {
          if (document.visibilityState === 'visible') {
            this.startAutoplay();
          } else {
            this.stopAutoplay();
          }
        });
        let resizeTimer;
        window.addEventListener('resize', () => {
          clearTimeout(resizeTimer);
          this.stopAutoplay();
          
          resizeTimer = setTimeout(() => {
            requestAnimationFrame(() => {
              this.updateSlider(false);
              this.startAutoplay();
            });
          }, 250);
        });
      }
    }
    try {
      const sliderContainer = document.querySelector('#testimonials-{{ section.id }}');
      if (sliderContainer) {
        new TestimonialSlider(sliderContainer);
      }
    } catch (error) {
      console.warn('Testimonial slider initialization error:', error);
    }
  })();
</script>
{% schema %}
{
  "name": "Testimonials Slider",
  "settings": [
    {
      "type": "header",
      "content": "Layout Settings"
    },
    {
      "type": "select",
      "id": "layout",
      "label": "Layout Style",
      "options": [
        {
          "value": "grid",
          "label": "Grid"
        },
        {
          "value": "carousel",
          "label": "Carousel"
        }
      ],
      "default": "carousel"
    },
    {
      "type": "range",
      "id": "desktop_slides",
      "min": 1,
      "max": 6,
      "step": 1,
      "label": "Slides per View (Desktop)",
      "default": 3
    },
    {
      "type": "range",
      "id": "tablet_slides",
      "min": 1,
      "max": 4,
      "step": 1,
      "label": "Slides per View (Tablet)",
      "default": 2
    },
    {
      "type": "select",
      "id": "mobile_slides",
      "label": "Slides per View (Mobile)",
      "options": [
        {
          "value": "1",
          "label": "1 Slide"
        },
        {
          "value": "2",
          "label": "2 Slides"
        }
      ],
      "default": "1"
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background Color",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "card_background",
      "label": "Card Background",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text Color",
      "default": "#333333"
    },
    {
      "type": "color",
      "id": "star_color",
      "label": "Star Rating Color",
      "default": "#ed5e84"
    },
    {
      "type": "color",
      "id": "nav_button_color",
      "label": "Navigation Button Color",
      "default": "#ed5e84"
    },
    {
      "type": "header",
      "content": "Animation Settings"
    },
    {
      "type": "range",
      "id": "autoplay_speed",
      "min": 2,
      "max": 10,
      "step": 0.5,
      "unit": "s",
      "label": "Autoplay Speed",
      "default": 5
    },
    {
      "type": "checkbox",
      "id": "enable_autoplay",
      "label": "Enable Autoplay",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "pause_on_hover",
      "label": "Pause on Hover",
      "default": true
    },
    {
      "type": "select",
      "id": "transition_effect",
      "label": "Transition Effect",
      "options": [
        {
          "value": "slide",
          "label": "Slide"
        },
        {
          "value": "fade",
          "label": "Fade"
        }
      ],
      "default": "slide"
    },
    {
      "type": "header",
      "content": "Card Style"
    },
    {
      "type": "range",
      "id": "card_radius",
      "min": 0,
      "max": 30,
      "step": 2,
      "unit": "px",
      "label": "Card Border Radius",
      "default": 18
    },
    {
      "type": "range",
      "id": "image_height",
      "min": 100,
      "max": 400,
      "step": 10,
      "unit": "px",
      "label": "Image Height",
      "default": 200
    },
    {
      "type": "select",
      "id": "text_alignment",
      "label": "Text Alignment",
      "options": [
        {
          "value": "left",
          "label": "Left"
        },
        {
          "value": "center",
          "label": "Center"
        },
        {
          "value": "right",
          "label": "Right"
        }
      ],
      "default": "left"
    },
    {
      "type": "header",
      "content": "Navigation"
    },
    {
      "type": "checkbox",
      "id": "show_arrows",
      "label": "Show Navigation Arrows",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "show_dots",
      "label": "Show Navigation Dots",
      "default": false
    },
    {
      "type": "header",
      "content": "Spacing"
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 100,
      "step": 5,
      "unit": "px",
      "label": "Padding Top",
      "default": 50
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 100,
      "step": 5,
      "unit": "px",
      "label": "Padding Bottom",
      "default": 50
    },
    {
      "type": "range",
      "id": "space_between",
      "min": 10,
      "max": 50,
      "step": 5,
      "unit": "px",
      "label": "Space Between Slides",
      "default": 20
    }
  ],
  "blocks": [
    {
      "type": "testimonial",
      "name": "Testimonial",
      "settings": [
        {
          "type": "image_picker",
          "id": "image",
          "label": "Testimonial Image"
        },
        {
          "type": "image_picker",
          "id": "author_image",
          "label": "Author Image"
        },
        {
          "type": "text",
          "id": "author",
          "label": "Author Name",
          "default": "Customer Name"
        },
        {
          "type": "range",
          "id": "rating",
          "min": 1,
          "max": 5,
          "step": 1,
          "label": "Rating",
          "default": 5
        },
        {
          "type": "textarea",
          "id": "text",
          "label": "Testimonial",
          "default": "Share customer testimonials to show your store's reputation."
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "DT Testimonials Slider",
      "blocks": [
        {
          "type": "testimonial"
        },
        {
          "type": "testimonial"
        },
        {
          "type": "testimonial"
        }
      ]
    }
  ]
}
{% endschema %}

Twee dingen om te weten. De slider start pas bij drie of meer testimonials, dat staat in de constructor. En een paar instellingen in het schema (layout, autoplay-snelheid, fade, dots) staan er wel in maar worden door het script nog niet gelezen. De autoplay staat vast op zes seconden in de code. Het Swiper-commentaar bovenaan is een overblijfsel; deze slider gebruikt zijn eigen script en heeft die library niet nodig.

4. Video-testimonials

De video-testimonials-snippet in de praktijk: scrollbare klantvideo's op de productpagina.

Korte klantvideo's in een horizontale strip, staand formaat, met een thumbnail en een play-icoon. Klik en de video speelt in de kaart zelf, gemuted, één tegelijk. Geen YouTube-embed, dus geen logo en geen traag iframe.

Dit is bewust een snippet en geen sectie. Daardoor kun je hem in elk blok renderen: in je product template, in een eigen sectie, in de cart drawer, in de footer. Maak onder Snippets een nieuw bestand video-testimonials-advanced en plak dit erin. De snippet laadt Swiper en Font Awesome zelf via een CDN.

411 regels codeToon code +
{% comment %}  Video Testimonials met Swiper.js
  Gebruik: {% render 'video-testimonials-advanced', block: block %}
  Ontwikkeld door Diek Thunnissen - https://diekthunnissen.nl
{% endcomment %}

{% liquid
  assign snippet_title = block.settings.title | default: "Loved by 100,000+ happy customers."
  assign title_size = block.settings.title_size | default: 'h6'
  assign title_alignment = block.settings.title_alignment | default: 'center'
  assign title_color = block.settings.title_color | default: '#222222'
  assign show_title = block.settings.show_title | default: true
  assign show_controls = block.settings.show_controls | default: true
  assign autoplay_first = block.settings.autoplay_first | default: true
  assign videos_per_view = block.settings.videos_per_view | default: 3
  assign aspect_ratio = block.settings.aspect_ratio | default: 0.5625

  # Collect videos that are set
  assign video_count = 0
  for i in (1..8)
    assign video_key = 'video_' | append: i
    if block.settings[video_key] != blank
      assign video_count = video_count | plus: 1
    endif
  endfor
%}

{% if video_count > 0 %}
<!-- Swiper CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />

<div class="videoblock">
  {% if snippet_title != blank and show_title %}
    <videotitle class="{{ title_size }}" style="margin-bottom: 20px; text-align: {{ title_alignment }}; display: block; color: {{ title_color }};">
      {{ snippet_title }}
    </videotitle>
  {% endif %}

  <div style="margin-top: 10px; margin-bottom: 10px;" class="video-slider">
    <div class="swiper Videoslider">
      <div class="swiper-wrapper">

        {% for i in (1..8) %}
          {% liquid
            assign video_key = 'video_' | append: i
            assign thumbnail_key = 'video_' | append: i | append: '_thumbnail'
            assign video = block.settings[video_key]
            assign thumbnail = block.settings[thumbnail_key]
          %}

          {% if video != blank %}
            <div class="swiper-slide">
              <div class="video-slide" data-video-id="{{ forloop.index }}">
                <div class="thumbnailimg">
                  <i class="fa-solid fa-circle-play"></i>
                  {% if thumbnail != blank %}
                    {{ thumbnail | image_url: width: 500 | image_tag:
                      class: 'video-thumbnail',
                      width: 135,
                      height: 240,
                      loading: 'lazy',
                      alt: 'Video Preview'
                    }}
                  {% elsif video.preview_image != blank %}
                    {{ video.preview_image | image_url: width: 500 | image_tag:
                      class: 'video-thumbnail',
                      width: 135,
                      height: 240,
                      loading: 'lazy',
                      alt: 'Video Preview'
                    }}
                  {% else %}
                    <div class="video-thumbnail video-thumbnail--placeholder">
                      <span>Video {{ i }}</span>
                    </div>
                  {% endif %}
                </div>

                <div class="video-container" style="display:none;">
                  {{ video | video_tag:
                    controls: true,
                    muted: true,
                    playsinline: true,
                    width: 135,
                    height: 240,
                    style: 'border-radius: 8px; width: 100%; height: 100%; object-fit: cover;'
                  }}
                </div>
              </div>
            </div>
          {% endif %}
        {% endfor %}

      </div>

      <!-- Swiper scrollbar -->
      <div class="swiper-scrollbar swiper-scrollbar_Videoslider"></div>
    </div>
  </div>
</div>

<style>
.videoblock {
  padding: 40px 0;
  width: 100%;
  max-width: 100%;
  overflow: hidden;
}

.h3 {
  font-size: 36px;
  font-weight: 600;
  margin: 0;
}

.h4 {
  font-size: 32px;
  font-weight: 600;
  margin: 0;
}

.h5 {
  font-size: 30px;
  font-weight: 500;
  margin: 0;
}

.h6 {
  font-size: 28px;
  font-weight: 500;
  margin: 0;
}

.body-lg {
  font-size: 20px;
  font-weight: 400;
  margin: 0;
}

.body {
  font-size: 16px;
  font-weight: 400;
  margin: 0;
}

.video-slider {
  position: relative;
  width: 100%;
}

.swiper {
  width: 100%;
  height: auto;
}

.swiper-slide {
  width: auto !important;
  margin-right: 9px !important;
}

.video-slide {
  position: relative;
  cursor: pointer;
  width: 135px;
  height: 240px;
  display: inline-block;
}

.thumbnailimg {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 8px;
  overflow: hidden;
}

.video-thumbnail {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.video-thumbnail--placeholder {
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 14px;
  font-weight: 500;
}

.fa-circle-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 48px;
  color: rgba(255, 255, 255, 0.9);
  z-index: 2;
  pointer-events: none;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}

.video-container {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.video-container video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Swiper scrollbar styling */
.swiper-scrollbar {
  position: relative !important;
  height: 4px !important;
  background: rgba(0, 0, 0, 0.08) !important;
  border-radius: 2px !important;
  margin-top: 20px !important;
}

.swiper-scrollbar-drag {
  background: linear-gradient(90deg, #4a4a4a, #2c2c2c) !important;
  border-radius: 2px !important;
  height: 100% !important;
  opacity: 0.8;
  transition: opacity 0.3s ease;
}

.swiper-scrollbar-drag:hover {
  opacity: 1;
}

/* Mobile responsiveness */
@media (max-width: 768px) {
  .videoblock {
    padding: 30px 0;
  }

  .h3 { font-size: 28px; }
  .h4 { font-size: 24px; }
  .h5 { font-size: 22px; }
  .h6 { font-size: 20px; }
  .body-lg { font-size: 18px; }

  .video-slider {
    padding: 0 20px;
  }

  .swiper {
    margin: 0 -20px;
    padding: 0 20px;
  }
}

@media (max-width: 480px) {
  .h3 { font-size: 24px; }
  .h4 { font-size: 22px; }
  .h5 { font-size: 20px; }
  .h6 { font-size: 18px; }

  .video-slide {
    width: 120px;
    height: 213px;
  }

  .fa-circle-play {
    font-size: 40px;
  }
}

/* Desktop - show more videos */
@media (min-width: 1024px) {
  .video-slide {
    width: 160px;
    height: 284px;
  }
}

@media (min-width: 1440px) {
  .video-slide {
    width: 180px;
    height: 320px;
  }
}
</style>

<!-- Font Awesome for play icon -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">

<!-- Swiper JS -->
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>

<script>
// Video play functionality
document.addEventListener('click', function(e) {
  // Check if clicking on a video slide
  const videoSlide = e.target.closest('.video-slide');
  if (!videoSlide) return;

  // Prevent multiple clicks
  if (videoSlide.classList.contains('playing')) return;

  const thumbnail = videoSlide.querySelector('.thumbnailimg');
  const videoContainer = videoSlide.querySelector('.video-container');
  const video = videoContainer?.querySelector('video');

  if (!video) {
    console.error('Video element not found');
    return;
  }

  // Hide thumbnail and show video
  thumbnail.style.display = 'none';
  videoContainer.style.display = 'block';
  videoSlide.classList.add('playing');

  // Reset video and play
  video.currentTime = 0;
  video.muted = true; // Ensure muted for autoplay

  // Try to play
  const playPromise = video.play();

  if (playPromise !== undefined) {
    playPromise.then(() => {
      console.log('Video is playing');
    }).catch(error => {
      console.error('Error playing video:', error);
      // If autoplay fails, show controls
      video.controls = true;
    });
  }

  // Stop all other videos
  document.querySelectorAll('.video-slide.playing').forEach(slide => {
    if (slide !== videoSlide) {
      const otherVideo = slide.querySelector('video');
      const otherThumbnail = slide.querySelector('.thumbnailimg');
      const otherContainer = slide.querySelector('.video-container');

      if (otherVideo && !otherVideo.paused) {
        otherVideo.pause();
        otherVideo.currentTime = 0;
      }

      if (otherThumbnail) otherThumbnail.style.display = 'block';
      if (otherContainer) otherContainer.style.display = 'none';
      slide.classList.remove('playing');
    }
  });

  // Handle video ended
  video.addEventListener('ended', function handleEnded() {
    thumbnail.style.display = 'block';
    videoContainer.style.display = 'none';
    videoSlide.classList.remove('playing');
    video.removeEventListener('ended', handleEnded);
  });
});

// Initialize Swiper when DOM is ready
function initVideoSwiper() {
  const swiper = new Swiper('.Videoslider', {
    slidesPerView: 'auto',
    spaceBetween: 9,
    freeMode: true,
    scrollbar: {
      el: '.swiper-scrollbar_Videoslider',
      draggable: true,
      dragSize: 'auto'
    },
    breakpoints: {
      320: {
        slidesPerView: 'auto',
        spaceBetween: 9
      },
      768: {
        slidesPerView: 'auto',
        spaceBetween: 12
      },
      1024: {
        slidesPerView: 'auto',
        spaceBetween: 15
      }
    }
  });
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initVideoSwiper);
} else {
  initVideoSwiper();
}

// Reinitialize when Shopify theme editor reloads sections
document.addEventListener('shopify:section:load', function() {
  setTimeout(initVideoSwiper, 100);
});
</script>

{% endif %}

Daarna koppel je het als blok aan je product template. Zoek in main-product.liquid de regel met case block.type en voeg daar dit toe:

{%- when 'video_testimonials' -%}
  <div class="product-video-testimonials" {{ block.shopify_attributes }}>
    {% render 'video-testimonials-advanced', block: block %}
  </div>

En in het schema van datzelfde bestand, binnen de blocks-array, dit bloktype:

229 regels codeToon code +
{
  "type": "video_testimonials",
  "name": "📹 Video Testimonials",
  "limit": 1,
  "settings": [
    {
      "type": "header",
      "content": "Titel instellingen"
    },
    {
      "type": "text",
      "id": "title",
      "label": "Titel",
      "default": "Wat klanten zeggen"
    },
    {
      "type": "checkbox",
      "id": "show_title",
      "label": "Toon titel",
      "default": true
    },
    {
      "type": "select",
      "id": "title_size",
      "label": "Titel grootte",
      "options": [
        {
          "value": "h3",
          "label": "Extra groot (H3)"
        },
        {
          "value": "h4",
          "label": "Groot (H4)"
        },
        {
          "value": "h5",
          "label": "Medium groot (H5)"
        },
        {
          "value": "h6",
          "label": "Medium (H6)"
        },
        {
          "value": "body-lg",
          "label": "Grote tekst"
        },
        {
          "value": "body",
          "label": "Normale tekst"
        }
      ],
      "default": "h6"
    },
    {
      "type": "select",
      "id": "title_alignment",
      "label": "Titel uitlijning",
      "options": [
        {
          "value": "left",
          "label": "Links"
        },
        {
          "value": "center",
          "label": "Gecentreerd"
        },
        {
          "value": "right",
          "label": "Rechts"
        }
      ],
      "default": "center"
    },
    {
      "type": "color",
      "id": "title_color",
      "label": "Titel kleur",
      "default": "#222222"
    },
    {
      "type": "header",
      "content": "Video instellingen"
    },
    {
      "type": "checkbox",
      "id": "show_controls",
      "label": "Toon video controls",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "autoplay_first",
      "label": "Autoplay eerste video",
      "default": true
    },
    {
      "type": "select",
      "id": "aspect_ratio",
      "label": "Video aspect ratio",
      "options": [
        {
          "value": "0.5625",
          "label": "16:9 (Landscape)"
        },
        {
          "value": "0.75",
          "label": "4:3 (Standard)"
        },
        {
          "value": "1.7778",
          "label": "9:16 (Portrait)"
        }
      ],
      "default": "1.7778"
    },
    {
      "type": "header",
      "content": "Video 1"
    },
    {
      "type": "video",
      "id": "video_1",
      "label": "Video 1"
    },
    {
      "type": "image_picker",
      "id": "video_1_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    },
    {
      "type": "header",
      "content": "Video 2"
    },
    {
      "type": "video",
      "id": "video_2",
      "label": "Video 2"
    },
    {
      "type": "image_picker",
      "id": "video_2_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    },
    {
      "type": "header",
      "content": "Video 3"
    },
    {
      "type": "video",
      "id": "video_3",
      "label": "Video 3"
    },
    {
      "type": "image_picker",
      "id": "video_3_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    },
    {
      "type": "header",
      "content": "Video 4"
    },
    {
      "type": "video",
      "id": "video_4",
      "label": "Video 4"
    },
    {
      "type": "image_picker",
      "id": "video_4_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    },
    {
      "type": "header",
      "content": "Video 5"
    },
    {
      "type": "video",
      "id": "video_5",
      "label": "Video 5"
    },
    {
      "type": "image_picker",
      "id": "video_5_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    },
    {
      "type": "header",
      "content": "Video 6"
    },
    {
      "type": "video",
      "id": "video_6",
      "label": "Video 6"
    },
    {
      "type": "image_picker",
      "id": "video_6_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    },
    {
      "type": "header",
      "content": "Video 7"
    },
    {
      "type": "video",
      "id": "video_7",
      "label": "Video 7"
    },
    {
      "type": "image_picker",
      "id": "video_7_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    },
    {
      "type": "header",
      "content": "Video 8"
    },
    {
      "type": "video",
      "id": "video_8",
      "label": "Video 8"
    },
    {
      "type": "image_picker",
      "id": "video_8_thumbnail",
      "label": "Custom thumbnail (optioneel)"
    }
  ]
}

Liever een losse sectie die je overal kunt plaatsen? Maak dan onder Sections een bestand video-testimonials.liquid met dezelfde settings-array:

<section class="vt-section page-width">
  {% render 'video-testimonials-advanced', block: section %}
</section>

{% schema %}
{
  "name": "📹 Video Testimonials",
  "settings": [
    // Hier dezelfde settings array als hierboven
  ],
  "presets": [
    {
      "name": "📹 Video Testimonials"
    }
  ]
}
{% endschema %}

In de theme editor zie je nu het blok Video Testimonials. Upload de video's via Files, als MP4, en houd ze klein. De oude post adviseerde onder de tien megabyte per video en rond de één tot twee megabyte per dertig seconden; 720p is meer dan genoeg voor een testimonial. Een eigen thumbnail is optioneel, anders pakt Shopify het eerste frame. Op mobiel speelt de video gemuted af om autoplay-restricties te omzeilen; lukt dat niet, dan toont de code alsnog de controls.

Werkt de carousel niet soepel, kijk dan of je thema al een eigen Swiper laadt. Twee instanties bijten elkaar.

Waar dit wel en niet voor is

De slider en de video's tonen echte klanten. Daar is niets aan te verdedigen, dat is gewoon je bewijs op de plek waar de twijfel zit.

De kijkers-teller en de verkoop-notificatie zijn iets anders. Zoals ze hier staan, tonen ze verzonnen getallen en verzonnen namen. Een bezoeker die dat doorheeft, gelooft de rest van je pagina ook niet meer. En het is niet alleen een vertrouwenskwestie: een melding als “X mensen kochten dit” die niet klopt, is een misleidende handelspraktijk onder het consumentenrecht. Sinds de Omnibus-richtlijn is de ACM daar scherper op.

Ons advies: gebruik die twee snippets als vormgeving en koppel ze aan echte data voordat je ze live zet. Het aantal bestellingen van de laatste 24 uur uit een metafield dat Flow bijwerkt. Het laatste bestelmoment per product. Dan zegt het blok iets wat waar is, en werkt het ook nog eens beter.

Social proof is bewijs. Zodra je het moet verzinnen, heb je een ander probleem dan je productpagina.

Veelgestelde vragen.

Kun je de kijkers-teller en de verkoop-notificatie koppelen aan echte cijfers?
Ja, maar niet met Liquid alleen. Een thema kan geen orders lezen. Laat Shopify Flow bij elke bestelling een metafield op het product bijwerken (bijvoorbeeld het aantal bestellingen van de laatste 24 uur en het laatste bestelmoment) en lees dat veld in de snippet. Een app proxy kan hetzelfde met meer detail.
Heb je hier apps of een betaald thema voor nodig?
Nee. Alle vier de snippets draaien in elk Online Store 2.0-thema, ook in Dawn. De kijkers-teller en de verkoop-notificatie zijn custom liquid-blokken, de testimonials-slider is een eigen sectie, de video-testimonials zijn een snippet plus een bloktype in je product template.
Waarom start de testimonials-slider niet?
De slider heeft minimaal drie testimonials nodig. Met minder blokken stopt de constructor bewust, omdat het oneindig doorlopen met klonen anders niet klopt. Voeg een derde testimonial toe of toon ze zonder slider.
Diek ThunnissenDiek ThunnissenFounder & lead developer. Bouwt, verbetert en migreert de Shopify-laag voor DTC- en B2B-merken. LinkedInPlan een gesprek

Technische Shopify-inzichten uit de praktijk.

Geen sales, wel techniek.