Testimonial Slider — Free HTML CSS JS Carousel Snippet

Testimonial Slider · Cards · Plain HTML, CSS & JS · Live preview

What's included

Features

CSS translateX(-N×100%) slide: flex layout, no cloning, pure transform
Auto-advance: 5s setInterval, resets on any user interaction
resetAuto(): clears and restarts interval after every user action
Infinite loop: ((idx%total)+total)%total handles negative wrap-around
Touch swipe: touchstart records X, touchend checks 50px threshold
Keyboard ArrowLeft/ArrowRight navigation on document keydown
Dynamic dots: built from slides.length, active dot scales up 1.3×
will-change: transform hint for GPU compositing on the track

About this UI Snippet

Testimonial Slider — Auto-Play Carousel, Touch Swipe, Keyboard Nav & Dot Indicators

Screenshot of the Testimonial Slider snippet rendered live

A testimonial slider cycles through customer quotes (each a testimonial card) automatically, showing social proof without requiring a long scroll on the page — for a static grid instead, see the testimonial masonry. It is one of the most common components on landing pages, pricing pages, and homepages — and one of the most frequently requested UI patterns. This snippet provides a complete testimonial carousel: auto-advancing every 5 seconds, CSS translateX slide animation, dot indicator controls, prev/next arrow buttons, keyboard arrow key navigation, and touch swipe support — all without any carousel library (see the general-purpose carousel for non-quote content).

The CSS translateX slide mechanism

All slides sit side by side in a single .slider-track flex row, each with flex: 0 0 100% to occupy exactly the full width. The track is translateX(-current × 100%) to show only the current slide. CSS transition: transform 0.45s cubic-bezier(0.4,0,0.2,1) animates between slides. overflow: hidden on the wrapper clips all slides except the currently visible one. No cloning, no absolute positioning — pure flex layout.

Auto-advance with reset on interaction

A setInterval calls slideTo(current + 1) every 5 seconds. Whenever the user interacts (clicks a dot, arrow, or swipes), resetAuto() clears the current interval and starts a new one. This ensures the 5-second timer always counts from the last user interaction, not the last auto-advance — preventing the timer from firing immediately after a user swipe.

Touch swipe detection

touchstart records the initial X position. touchend computes the delta between start and end. If the absolute delta exceeds 50px (a minimum swipe distance threshold), the slider advances in the swipe direction. The passive: true option on touchstart allows the browser to scroll while tracking the gesture.

Infinite looping

The modulo calculation ((idx % total) + total) % total handles negative indices and wrap-around. Sliding left from index 0 gives (((-1) % 4) + 4) % 4 = 3, wrapping to the last slide. Sliding right from index 3 gives ((4 % 4) + 4) % 4 = 0, wrapping to the first.

Dot indicators

Dots are created dynamically from the slide count. The active dot scales up (transform: scale(1.3)) and turns indigo. Clicking a dot calls slideTo(i) directly, navigating to that specific slide and resetting the auto-advance timer.

Adding pause on hover

Improve usability by pausing auto-advance when the user hovers the slider: track.addEventListener("mouseenter", () => clearInterval(autoTimer)); track.addEventListener("mouseleave", resetAuto). This gives users time to read a testimonial they are currently viewing without the slider advancing while their cursor is on it.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work out the swipe-threshold or wrap-around math by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the touch handler only advances the slide once the horizontal delta between touchstart and touchend exceeds 50 pixels, and how the double-modulo expression in slideTo correctly wraps negative indices without an if-statement. The same assistant can help optimize it — for instance whether resetAuto tearing down and rebuilding the interval on every single navigation (including the very first auto-advance) is the cleanest way to keep the timer in sync, or whether a vertical scroll gesture could accidentally be misread as a horizontal swipe on some devices. It's also useful for extending the slider: ask it to add a hover-to-pause behavior alongside the existing keyboard and touch support, show a countdown progress bar under each slide, or make it responsive to show two testimonials per view on wider screens. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an auto-advancing "testimonial slider" in plain HTML, CSS, and JavaScript with keyboard navigation and touch swipe support — no carousel library, no framework.

Requirements:
- A flex track of slides inside an overflow-hidden wrapper, where every slide takes exactly 100% of the track's width, and the visible slide is controlled entirely by a CSS transition on the track's transform property.
- A single slideTo(index) function that normalizes the given index into valid bounds using a double-modulo expression so both forward overflow and negative (backward) indices wrap correctly, updates the track's transform, and marks the corresponding dot indicator active — every navigation path (arrow buttons, dot clicks, autoplay, keyboard, and touch swipe) must call this one function.
- Dot indicators built dynamically from the actual number of slide elements found in the DOM (not a hardcoded count), so adding or removing a slide from the markup automatically changes the dot count with no JavaScript edits.
- An auto-advance timer on a fixed interval that is fully cleared and recreated (not just left running) inside slideTo, so every manual navigation resets the countdown and the next auto-advance is always a full interval away from the last change, whether it was triggered by a user or by the timer itself.
- A document-level keydown listener mapping the left and right arrow keys to going to the previous and next slide respectively.
- Touch handling that records the horizontal touch position on touchstart, computes the horizontal distance moved by touchend, and only triggers a slide change if that distance exceeds a minimum pixel threshold (to avoid accidental navigation from small movements or vertical scrolling), advancing in the direction of the swipe.
- Prev/next buttons that must never be permanently disabled since the slider loops infinitely in both directions.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<section class="section">
  <div class="section-head">
    <div class="eyebrow">Customer stories</div>
    <h2 class="section-title">Loved by thousands of teams</h2>
  </div>

  <div class="slider-wrap" id="slider-wrap">
    <div class="slider-track" id="slider-track">

      <div class="slide">
        <div class="quote-icon">"</div>
        <p class="quote-text">This tool completely transformed how our team ships features. We went from 2-week cycles to shipping every day. The productivity gain is real — I can't imagine going back.</p>
        <div class="author-row">
          <div class="author-av" style="background:linear-gradient(135deg,#6366f1,#a78bfa)">SK</div>
          <div class="author-info">
            <div class="author-name">Sarah Kim</div>
            <div class="author-role">CTO · Orbital Labs</div>
          </div>
          <div class="stars">★★★★★</div>
        </div>
      </div>

      <div class="slide">
        <div class="quote-icon">"</div>
        <p class="quote-text">We evaluated five platforms. This was the only one where the team actually got excited about using it. Onboarding took one afternoon, and we haven't looked back since.</p>
        <div class="author-row">
          <div class="author-av" style="background:linear-gradient(135deg,#ec4899,#f97316)">MJ</div>
          <div class="author-info">
            <div class="author-name">Marcus Johnson</div>
            <div class="author-role">VP Engineering · Nexus</div>
          </div>
          <div class="stars">★★★★★</div>
        </div>
      </div>

      <div class="slide">
        <div class="quote-icon">"</div>
        <p class="quote-text">The support team is extraordinary. When we had a question at 11pm, we had a detailed response within the hour. That kind of responsiveness is rare and it matters enormously to us.</p>
        <div class="author-row">
          <div class="author-av" style="background:linear-gradient(135deg,#10b981,#0ea5e9)">AL</div>
          <div class="author-info">
            <div class="author-name">Aisha Laurent</div>
            <div class="author-role">Head of Product · Pulse</div>
          </div>
          <div class="stars">★★★★★</div>
        </div>
      </div>

      <div class="slide">
        <div class="quote-icon">"</div>
        <p class="quote-text">ROI was positive within the first month. We eliminated three separate tools and reduced our monthly SaaS bill by 40%. It pays for itself several times over.</p>
        <div class="author-row">
          <div class="author-av" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">RP</div>
          <div class="author-info">
            <div class="author-name">Raj Patel</div>
            <div class="author-role">CEO · Vertex Inc</div>
          </div>
          <div class="stars">★★★★★</div>
        </div>
      </div>

    </div>
  </div>

  <div class="slider-controls">
    <button class="ctrl-btn prev" id="prev-btn" onclick="slideTo(current-1)" aria-label="Previous testimonial">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg>
    </button>
    <div class="dots" id="dots"></div>
    <button class="ctrl-btn next" id="next-btn" onclick="slideTo(current+1)" aria-label="Next testimonial">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>
    </button>
  </div>
</section>

Step by step

How to Use

  1. 1
    Watch the auto-advance and interact with controlsThe slider advances every 5 seconds. Click the ‹ › arrows or dots to navigate manually — the auto-advance timer resets after any interaction. Use keyboard arrow keys for navigation.
  2. 2
    Swipe on touch devicesOn mobile, swipe left to advance and right to go back. The 50px minimum swipe distance prevents accidental navigation during vertical scroll.
  3. 3
    Replace the testimonial contentEdit each .slide div: update .quote-text, .author-av initials and gradient, .author-name, .author-role, and optionally the .stars count. Each slide is self-contained.
  4. 4
    Add or remove slidesDuplicate a .slide div or delete one. The JavaScript reads slides.length dynamically, so the dot count and loop bounds update automatically. No code changes needed.
  5. 5
    Change the auto-advance speedUpdate 5000 in the setInterval call to any millisecond value. Set to 0 or remove the setInterval call entirely to disable auto-advance for a user-only controlled slider.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useState for current index and useEffect for the auto-advance interval, or "Tailwind" for a Tailwind CSS version.

Real-world uses

Common Use Cases

Landing page social proof and customer testimonials
The testimonial slider is the most common social proof pattern on SaaS and product landing pages. Auto-advancing shows multiple testimonials without requiring scroll. The five-star rating, customer name, and company role build credibility rapidly.
Pricing page trust-building testimonials section
Testimonials placed near the pricing section reduce purchase hesitation. The slider format lets you show 4+ testimonials in the space of one card. Use guarantee-specific and ROI-specific quotes near the pricing tiers for maximum conversion impact.
Product tour and feature highlight showcase
Adapt the slider to show product screenshots or feature highlights instead of text testimonials. Each slide becomes a feature: image/screenshot, headline, description. The auto-advance and swipe create a guided product tour.
Portfolio case study and project showcase
Freelancers and agencies can use the testimonial slider for client quotes on their portfolio page. The gradient avatar initials work when client photos are not available, maintaining a polished appearance.
Study the CSS-only slider architecture
The flex row + translateX approach is the cleanest carousel implementation pattern — no absolute positioning, no cloning, no complex DOM manipulation. Studying how overflow:hidden clips the track and how translateX moves to any slide teaches the core carousel technique.
Onboarding and welcome screen feature carousel
Use the slider for new user onboarding: each slide shows a product feature with illustration and description. The dots communicate how many steps remain. The swipe support is essential for mobile onboarding flows.

Got questions?

Frequently Asked Questions

The resetAuto() function calls clearInterval(autoTimer) to stop the current interval, then immediately calls setInterval(() => slideTo(current+1), 5000) to start a fresh 5-second timer. This is called inside slideTo() on every navigation — user-triggered or auto-triggered. The result: the timer always counts 5 full seconds from the last slide change, regardless of when it happened. Without the clearInterval, a user swipe near the end of a 5-second cycle could cause an immediate second advance.

Change the .slider-track from display:flex to position:relative. Make each .slide position:absolute; inset:0; opacity:0; transition:opacity 0.5s. Add an .active class with opacity:1 to the current slide. In slideTo(), add/remove .active: slides.forEach((s,i)=>s.classList.toggle("active",i===current)). Remove the translateX logic entirely. The .slider-wrap needs position:relative and height matching the slide height.

Add a .progress div at the bottom of each slide: <div class="progress-bar"><div class="progress-fill" id="progress"></div></div>. CSS: .progress-fill { height:2px; background:#6366f1; animation: progress 5s linear; } @keyframes progress { from{width:0} to{width:100%} }. In slideTo(), reset the animation: const pbar = document.getElementById("progress"); pbar.style.animation="none"; pbar.offsetHeight; pbar.style.animation="".

Click "JSX" to download. Manage current with useState(0). The auto-advance uses useEffect: const id = setInterval(() => setCurrent(c => (c+1)%total), 5000); return () => clearInterval(id) — cleanup on unmount. Reset the interval on user interaction by tracking a dependency. Compute the track style: {transform: translateX(-${current*100}%)}. Build dots from Array.from({length:total},(_,i)=>i) and apply onClick and active class per index.