Let's Talk

How to Build a Lightning-Fast Review Carousel in HubSpot (Zero Dependencies)

By Tomislaw Dalic October 30, 2025

Tired of Embla, Swiper, or Slick failing in production?
CDNs blocked? Scripts not loading? Infinite scroll breaking everything?
This is the fix you’ve been waiting for.

A 5KB, offline-first, vanilla JS carousel that works every time — no external libraries, no HTTP requests, no debugging nightmares.


Why Ditch Carousel Libraries?

Problem With Libraries With Vanilla
CDN blocked by ORB/AdBlock Failed Works
Extra 50–200KB + HTTP request Slower 5KB
Version conflicts Possible Impossible
Overkill features Bloat Only what you need
Hard to debug in HubSpot Yes console.log heaven

For a review carousel, you need 3 things: slide, wait 5s, repeat.
Everything else is noise.


What You’ll Build

  • Auto-play (5s)
  • Clickable dots
  • Mobile swipe
  • Hover to pause
  • Smooth transitions
  • Multiple carousels per page
  • Zero external dependencies

Step 1: HTML Structure (HubL-Ready)

<div class="review-carousel">
  <div class="review-carousel__viewport">
    <div class="review-carousel__track">
      <div class="review-slide">
        <p class="review-quote">"Beste lease-ervaring ooit!"</p>
        <div class="review-author">
          <strong>Mark de Vries</strong> <span>★★★★★</span>
        </div>
      </div>
      <div class="review-slide">
        <p class="review-quote">"Snelle service, heldere communicatie."</p>
        <div class="review-author">
          <strong>Linda Bakker</strong> <span>★★★★★</span>
        </div>
      </div>
      <!-- Add more slides -->
    </div>
  </div>
  <div class="review-carousel__dots"></div>
</div>

Pro Tip: Use this inside a {% for %} loop in HubSpot to inject reviews dynamically.


Step 2: CSS (Inline in {% block footer_js %})

/* === CAROUSEL STYLES === */
.review-carousel {
  overflow: hidden;
  position: relative;
  background: #fff;
  border: 2px solid #00A3E0;
  border-radius: 16px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
  margin: 24px 0;
  padding: 32px 24px;
  max-width: 900px;
  margin-left: auto;
  margin-right: auto;
}

.review-carousel__viewport { overflow: hidden; border-radius: 12px; }

.review-carousel__track {
  display: flex;
  width: 100%;
  transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: transform;
}

.review-slide {
  min-width: 100%;
  flex-shrink: 0;
  padding: 0 16px;
  box-sizing: border-box;
  text-align: center;
}

.review-quote {
  font-size: 1.125rem;
  line-height: 1.6;
  margin: 0 0 16px;
  position: relative;
  font-style: italic;
  color: #1c2c4c;
}

.review-quote::before {
  content: '"';
  font-size: 4rem;
  color: #00A3E0;
  opacity: 0.2;
  position: absolute;
  left: -10px;
  top: -20px;
  font-family: Georgia, serif;
}

.review-author { font-size: 0.975rem; color: #555; }
.review-author strong { color: #1c2c4c; display: block; margin-bottom: 4px; }

.review-carousel__dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 24px;
  padding: 0;
}

.review-carousel__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #cbd5e1;
  border: none;
  cursor: pointer;
  transition: all 0.3s ease;
  padding: 0;
}

.review-carousel__dot.active {
  background: #00A3E0;
  width: 28px;
  border-radius: 6px;
}

.review-carousel__dot:hover { background: #00A3E0; opacity: 0.8; }

/* Mobile */
@media (max-width: 768px) {
  .review-slide { padding: 0 12px; }
  .review-quote { font-size: 1rem; }
}

Step 3: JavaScript (Paste in {% block footer_js %})

<script>
document.addEventListener('DOMContentLoaded', () => {
  console.log('Review Carousel: Initializing...');

  const carousels = document.querySelectorAll('.review-carousel');
  console.log(`Found ${carousels.length} carousel(s)`);

  carousels.forEach((carousel, idx) => {
    const track = carousel.querySelector('.review-carousel__track');
    const slides = track.querySelectorAll('.review-slide');
    const dotsContainer = carousel.querySelector('.review-carousel__dots');

    if (slides.length < 2) {
      console.log(`Carousel ${idx + 1}: Skipped (only 1 slide)`);
      return;
    }

    let current = 0;
    let autoplay;

    // === CREATE DOTS ===
    dotsContainer.innerHTML = '';
    const dots = [];
    slides.forEach((_, i) => {
      const dot = document.createElement('button');
      dot.className = 'review-carousel__dot';
      if (i === 0) dot.classList.add('active');
      dot.setAttribute('aria-label', `Go to slide ${i + 1}`);
      dot.addEventListener('click', () => goTo(i));
      dotsContainer.appendChild(dot);
      dots.push(dot);
    });

    // === NAVIGATION ===
    const goTo = (index) => {
      current = (index + slides.length) % slides.length;
      track.style.transform = `translateX(-${current * 100}%)`;
      
      dots.forEach((dot, i) => {
        dot.classList.toggle('active', i === current);
      });

      console.log(`Carousel ${idx + 1}: Slide ${current + 1}/${slides.length}`);
    };

    const next = () => goTo(current + 1);

    // === AUTOPLAY ===
    const start = () => {
      stop();
      autoplay = setInterval(next, 5000);
    };
    const stop = () => clearInterval(autoplay);

    // === TOUCH SWIPE ===
    let touchStartX = 0;
    carousel.addEventListener('touchstart', e => {
      touchStartX = e.touches[0].clientX;
      stop();
    }, { passive: true });

    carousel.addEventListener('touchend', e => {
      const touchEndX = e.changedTouches[0].clientX;
      const diff = touchStartX - touchEndX;
      if (Math.abs(diff) > 50) {
        diff > 0 ? next() : goTo(current - 1);
      }
      start();
    }, { passive: true });

    // === HOVER PAUSE ===
    carousel.addEventListener('mouseenter', stop);
    carousel.addEventListener('mouseleave', start);

    // === INIT ===
    goTo(0);
    start();

    console.log(`Carousel ${idx + 1}: Initialized with ${slides.length} slides`);
  });
});
</script>

HubSpot Setup Checklist

{{ content_for_footer_js }} in base.html Yes
{% block footer_js %}{% endblock %} defined Yes
No Embla/Swiper/Slick scripts Yes
Hard refresh after publish Yes

Performance Stats

Total Size ~5KB
HTTP Requests 0
CLS Impact 0
LCP Friendly Yes
Works Offline Yes

Debug Like a Pro

Open DevTools → Console → You’ll see:

Review Carousel: Initializing...
Found 23 carousel(s)
Carousel 1: Initialized with 4 slides
Carousel 1: Slide 2/4
Carousel 1: Slide 3/4
...

No logs? → Carousel HTML not rendered.
No movement? → Check overflow: hidden and min-width: 100%.


Customization Ideas

// Faster autoplay
autoplay = setInterval(next, 3000);

// Add arrows
const prevBtn = document.createElement('button');
prevBtn.innerHTML = '❮';
prevBtn.addEventListener('click', () => goTo(current - 1));

// Fade instead of slide
.review-carousel__track { transition: opacity 0.5s; }
.review-slide { position: absolute; opacity: 0; }
.review-slide.active { opacity: 1; }

Final Thoughts

You don’t need a library to move a div 100% to the left.

This carousel:
Loads instantly
Never breaks
Works on every device
Is 100% yours

No more:
EmblaCarousel is not defined
Blocked by ORB
Script failed to load

Just clean, fast, reliable code.

 


Deploy this today. Your users (and Lighthouse score) will thank you.

Want more HubSpot performance hacks?
Drop a comment:
“Show me how to lazy-load 300+ car cards without jank!”
I’ll write it next.

Tomislaw Dalic

Join the Conversation

Share your thoughts and connect with other readers