SCSS

by Jordan Sayner

HTML

<div class="testimonial-slider" data-testimonial-slider>
  <div class="testimonial-slider__content">
    <div class="testimonial-slider__list" role="group" aria-live="polite">

      <!-- Slide 1 -->
      <article class="testimonial-slider__item is-active">
        <div class="ts-grid">
          <figure class="ts-media">
            <img src="https://picsum.photos/800/600?random=11" alt="">
          </figure>
          <div class="ts-copy">
            <blockquote class="testimonial-slider__quote">
              <p data-anim>
                She often describes her sessions as her ‘drama family’ and knows that,
                apart from her actual home, this is also a place that feels like home to her.
              </p>
            </blockquote>
            <p class="testimonial-slider__meta" data-anim>Youth Theatre Parent</p>
          </div>
        </div>
      </article>

      <!-- Slide 2 -->
      <article class="testimonial-slider__item">
        <div class="ts-grid">
          <figure class="ts-media">
            <img src="https://picsum.photos/800/600?random=22" alt="">
          </figure>
          <div class="ts-copy">
            <blockquote class="testimonial-slider__quote">
              <p data-anim>
                The confidence and friendships our son has found here are incredible.
                He counts down the days to each session.
              </p>
            </blockquote>
            <p class="testimonial-slider__meta" data-anim>Parent of Participant</p>
          </div>
        </div>
      </article>

      <!-- Slide 3 -->
      <article class="testimonial-slider__item">
        <div class="ts-grid">
          <figure class="ts-media">
            <img src="https://picsum.photos/800/600?random=33" alt="">
          </figure>
          <div class="ts-copy">
            <blockquote class="testimonial-slider__quote">
              <p data-anim>
                Workshops feel...

SCSS

/* ===============================
   Testimonial Slider — clean SCSS
   =============================== */

.testimonial-slider {
  /* Design tokens */
  --radius: 24px;
  --pad: clamp(16px, 3vw, 32px);
  --gap: clamp(16px, 4vw, 40px);
  --fade: 420ms;
  --e: cubic-bezier(.22,.61,.36,1);

  --bg: #fff;
  --ink: #222;
  --ink-subtle: #5b5b5b;
  --brand: #8E1B7A;
  --brand-2: #cfa3d2;

  color: var(--ink);
  background: var(--bg);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow:
    0 1px 2px rgba(0,0,0,.05),
    0 8px 40px rgba(0,0,0,.12);

  /* CONTENT WRAPPER
     Two rows: slide list, then controls */
  &__content {
    display: grid;
    grid-template-rows: auto auto;
    gap: 16px;
    padding: var(--pad);
    background: radial-gradient(120% 140% at 100% 100%, #ffd7e0 0%, #fff2b8 40%, #fff 75%);
  }

  /* SLIDE LIST
     JS updates height for smooth auto-height */
  &__list {
    position: relative;
    min-height: 320px; /* avoids flash before JS measures */
    transition: height var(--fade) var(--e);
    z-index: 1;
  }

  /* SLIDE */
  &__item {
    position: absolute;
    inset: 0;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--fade) var(--e);

    &.is-active {
      position: relative;
      opacity: 1;
      pointer-events: auto;
    }

    /* internal grid per slide: image left, copy right */
    .ts-grid {
      display: grid;
      grid-template-columns: 1.1fr 1fr;
      gap: var(--gap);
      align-items: stretch;

      @media (max-width: 900px) {
        grid-template-columns: 1fr;
      }
    }

    .ts-media {
      position: relative;
      min-height: 420px;
      border-radius: 16px;
      overflow: hidden;
      background: #111;

      @media (max-width: 900px) { min-height: 260px; }

      img {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        object-fit: cover;
...

JavaScript

const root = document.querySelector('[data-testimonial-slider]');

  const list   = root.querySelector('.testimonial-slider__list');
  const slides = Array.from(root.querySelectorAll('.testimonial-slider__item'));
  const dots   = Array.from(root.querySelectorAll('.testimonial-slider__pagination button'));
  const prev   = root.querySelector('.ts-arrow--prev');
  const next   = root.querySelector('.ts-arrow--next');

  let index = Math.max(0, slides.findIndex(s => s.classList.contains('is-active')));

  function setListHeight(slide) {
    list.style.height = slide.offsetHeight + 'px';
  }

  function prepTextAnimation(slide) {
    slide.classList.remove('is-in'); // reset
    // force reflow to restart transitions
    void slide.offsetWidth;
    // stagger
    slide.querySelectorAll('[data-anim]').forEach((el, i) => {
      el.style.transitionDelay = (i * 70) + 'ms';
    });
    slide.classList.add('is-in');
  }

  function updateDots() {
    dots.forEach((d, i) => d.classList.toggle('is-active', i === index));
  }

  function goTo(newIndex) {
    if (newIndex === index || newIndex < 0 || newIndex >= slides.length) return;

    const oldSlide = slides[index];
    const newSlide = slides[newIndex];

    // height before reveal (so it animates to new height)
    setListHeight(newSlide);

    // swap visibility
    oldSlide.classList.remove('is-active', 'is-in');
    newSlide.classList.add('is-active');
    prepTextAnimation(newSlide);

    index = newIndex;
    updateDots();
  }

  // Controls
  prev.addEventListener('click', () => goTo((index - 1 + slides.length) % slides.length));
  next.addEventListener('click', () => goTo((index + 1) % slides.length));
  dots.forEach((dot, i) => dot.addEventListener('click', () => goTo(i)));

  // Init
  setListHeight(slides[index]);
  requestAnimationFrame(() => prepTextAnimation(slides[index]));
  updateDots();

  // Resize sync
  let rAF;
 ...