How to Build a Lightning-Fast Review Carousel in HubSpot (Zero Dependencies)
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.
Join the Conversation
Share your thoughts and connect with other readers