Search code examples
javascriptcssflexboxcarousel

Same height dynamic "carousel slides" without expanding parent


I'm trying to create a carousel with arbitrary text on each "slide", where all of the slides maintain the same (i.e., the largest) height. I'm writing it myself, so I can get away with anything (e.g., doing it all in JavaScript), but I'd prefer it be as simple as possible, so lean towards more CSS, less JS.

I have it all working as a carousel, but can't get each of the slides to be the same height. I'm currently using flexbox on the parent container and JS to change a style which changes opacity and width to make only 1 slide visible at a time, while maintaining height.

HTML:

<div class="testimonial-container"> // the parent — flex ... I'm growing when I shouldn't be
  <div class="testimonial active"> // the child, also flex, because I don't know what's inside; direction: column
    <div class="image-wrap">&nbsp;</div>
    <div class="testimonial-content">“Hi! I'm Jim!” // but I could be 10 lines of text
    </div>
    <div class="reviewer">Jim Jones</div>
    <div class="jobrole">Bad A-- Hombre!</div>
  </div>
  ... // more testimonials
</div>
<div class="testimonial-dots">
  <span class="testimonial-dot active"></span> // I'm a button
  ... // more dots
</div>

CSS:

.testimonial-container {
        max-width: 100%;
        position: relative;
        margin: auto;
        display: flex;     
}

/* hide the testimonial by default */
.testimonial-container div.testimonial {
        opacity: 0;
        width: 0px;
}

/* visible when JS adds "active" class */    
.testimonial-container div.testimonial.active {
        opacity: 1;
        width: 100%;
}  

.testimonial {
        display: flex;
        flex-direction: column;
}

.image-wrap,
.testimonial-content,
.reviewer,
.jobrole {
        padding: 8px 12 px;
        width: 100%;
        text-align: center;
}

The script:

  let testimonialIndex = 1;
  let testimonials, dots;
  
  window.addEventListener( 'DOMContentLoaded', () => {
    testimonials = document.getElementsByClassName("testimonial");
    dots = document.getElementsByClassName("testimonial-dot");
  
    for ( let i = 0; i < dots.length; i++ ) {
      dots[i].onclick = () => currentTestimonial(i + 1);
    }
  
    showTestimonial(testimonialIndex);
  } );
  
  // dot image controls
  function currentTestimonial(n) {
    showTestimonial(testimonialIndex = n);
  }
  
  function showTestimonial(n) {
    if (n > testimonials.length) {testimonialIndex = 1};
    if (n < 1) {testimonialIndex = testimonials.length};
  
    for ( let i = 0; i < testimonials.length; i++) {
      testimonials[i].classList.remove('active');
    }
    for ( let i = 0; i < dots.length; i++ ) {
      dots[i].className = dots[i].className.replace(" active", "");
    }
  
    const testimonial = testimonials[testimonialIndex-1];
  
    testimonial.classList.add('active');
  
    dots[testimonialIndex-1].className += " active";
  }

When it loads, it's fine (the first slide is the correct height). The problem is, when I execute the JS to select a different slide, the parent expands to be the height of all the slides stacked on top of each other (actually, to much more than that).

As with all such things, this started out with "I'll just use a WordPress plugin", and ended with, "Gak! These don't work, and they're bloated and obfuscated! I'll write my own!"

So I'm down the path of trying to write a very simple and lightweight testimonial carousel. I started with W3 Schools "How TO - Slideshow". That was great, and got me almost all the way there, but the problem was, they're testimonials, so the text is arbitrary length, and I didn't want the carousel changing height as you flip between slides.

Googling around got me to this SE question, which, aside from being jQuery, seemed perfect. The W3 Schools example basically toggled display: none. This SE answer got me to the flex model and using width: 0px; opacity: 0; to hide things. But now I'm stuck. In his answer, he has three buttons below the "carousel" and it seems to work fine. But in my implementation, when you click on a button, the parent div expands and pushes the buttons way off the screen.

Here's a fiddle; to see it in action, just click on one of the buttons and watch the buttons disappear (they're now like 2000px below the screen ... I don't know what that is; it's not even the height of the two testimonials stacked on top of each other, maybe it's some version of infinity).

How do I stop that parent from expanding?

let testimonialIndex = 1;
  let testimonials, dots;
  
  window.addEventListener( 'DOMContentLoaded', () => {
    testimonials = document.getElementsByClassName("testimonial");
    dots = document.getElementsByClassName("testimonial-dot");
  
    for ( let i = 0; i < dots.length; i++ ) {
      dots[i].onclick = () => currentTestimonial(i + 1);
    }
  
    showTestimonial(testimonialIndex);
  } );
  
  // dot image controls
  function currentTestimonial(n) {
    showTestimonial(testimonialIndex = n);
  }
  
  function showTestimonial(n) {
    if (n > testimonials.length) {testimonialIndex = 1};
    if (n < 1) {testimonialIndex = testimonials.length};
  
    for ( let i = 0; i < testimonials.length; i++) {
      testimonials[i].classList.remove('active');
    }
    for ( let i = 0; i < dots.length; i++ ) {
      dots[i].className = dots[i].className.replace(" active", "");
    }
  
    const testimonial = testimonials[testimonialIndex-1];
  
    testimonial.classList.add('active');
  
    dots[testimonialIndex-1].className += " active";
  }
.testimonial-container {
        max-width: 100%;
        position: relative;
        margin: auto;
        display: flex;     
}

/* hide the testimonial by default */
.testimonial-container div.testimonial {
        opacity: 0;
        width: 0px;
}

.testimonial-container div.testimonial.active {
        opacity: 1;
        width: 100%;
}  

.testimonial {
        display: flex;
        flex-direction: column;
}

.image-wrap,
.testimonial-content,
.reviewer,
.jobrole {
        padding: 8px 12 px;
        width: 100%;
        text-align: center;
}

/** below just for aesthetics */
.image-wrap {
  display: block;
  width: 150px;
  height: 150px;
  margin: 0 auto;
  }
  
  .testimonial-dots {
        text-align: center;
}

.testimonial-dot {
        cursor: pointer;
        height: 15px;
        width: 15px;
        background-color: #bbb;
        border-radius: 50%;
        display: inline-block;
        margin: 0 2.5px;
}

.testimonial-dot.active,
.testimonial-dot:hover {
  background-color: #717171;
}
<div class="testimonial-container">
  <div class="testimonial">
    <div class="image-wrap" style="background-color: #0f0;">&nbsp;</div>
    <div class="testimonial-content">“Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla consectetur a velit ac luctus. Sed mattis finibus massa nec pretium. In mattis finibus pharetra. Fusce aliquam id neque accumsan accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras lacinia quam est, sit amet dignissim augue lacinia eget. Donec sed tempor elit, eu tempus augue. Duis ac elit justo. Ut ex metus, semper a ultricies congue, tincidunt tristique leo. Ut porta sodales fermentum. Vivamus et ante ullamcorper, porta sem at, luctus mauris. Ut fermentum velit quis orci eleifend, sed auctor elit tincidunt. Maecenas in magna et augue tristique cursus. Duis consectetur vel neque a fermentum.”
    </div>
    <div class="reviewer">Jim Jones</div>
    <div class="jobrole">Bad A-- Hombre!</div>
  </div>
  <div class="testimonial active">
    <div class="image-wrap" style="background-color: #00f;">&nbsp;</div>
    <div class="testimonial-content">“I'm verbose!”</div>
    <div class="reviewer">Bob Smith</div>
  </div>
</div>
<!-- Testimonial container -->
<!-- dots/circles -->
<div class="testimonial-dots">
  <span class="testimonial-dot"></span>
  <span class="testimonial-dot active"></span>
</div>


Solution

  • Generally when you do a carousel type thing, you have two containers. One container is the width of the viewport, and the inside container is the width of the content itself. Then you slide the content container left/right based on the currently selected item.

    If you /wanted/ to do it your way, you might be able to use something like height: max-content on your container, which will make it constrained to be no larger than the largest item inside. Here's my solution using the two containers:

    // Greatly simplifed your javascript.
    
    const container = document.querySelector('.testimonial-container');
    const dotsContainer = document.querySelector('.testimonial-dots');
    const dots = [...dotsContainer.children];
    
    // We add an event listener on the parent container to the dots.
    dotsContainer.addEventListener('click', ({
      target
    }) => {
      // If the item clicked was not a dot, don't do anything
      if (!target.matches('.testimonial-dot')) return;
      const myIndex = dots.indexOf(target);
      // Move the container -100% * the dot index
      const offset = -100 * myIndex;
      container.style.transform = `translateX(${-100 * myIndex}%)`;
      dots.map(d => d.classList.remove('active'));
      target.classList.add('active');
    });
    .carousel {
      /* hide off-screen items */
      overflow: hidden;
    }
    
    .testimonial-container {
      display: flex;
    }
    
    .testimonial-container div.testimonial {
      /* make all items bre 100% in width */
      min-width: 100%;
    }
    
    
    /* the rest of the css is untouched */
    
    .image-wrap,
    .testimonial-content,
    .reviewer,
    .jobrole {
      padding: 8px 12 px;
      width: 100%;
      text-align: center;
    }
    
    
    /** below just for aesthetics */
    
    .image-wrap {
      display: block;
      width: 150px;
      height: 150px;
      margin: 0 auto;
    }
    
    .testimonial-dots {
      text-align: center;
    }
    
    .testimonial-dot {
      cursor: pointer;
      height: 15px;
      width: 15px;
      background-color: #bbb;
      border-radius: 50%;
      display: inline-block;
      margin: 0 2.5px;
    }
    
    .testimonial-dot.active,
    .testimonial-dot:hover {
      background-color: #717171;
    }
    <!-- This is the container that contains all the items and only shows the active one. It's like a viewport -->
    <div class="carousel">
      <!-- This contains all the items, and the width is equal to each item's width added together. This container gets a translateX applied to it when the active item changes -->
      <div class="testimonial-container">
        <div class="testimonial">
          <div class="image-wrap" style="background-color: #0f0;">&nbsp;</div>
          <div class="testimonial-content">“Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla consectetur a velit ac luctus. Sed mattis finibus massa nec pretium. In mattis finibus pharetra. Fusce aliquam id neque accumsan accumsan. Class aptent taciti sociosqu ad litora torquent
            per conubia nostra, per inceptos himenaeos. Cras lacinia quam est, sit amet dignissim augue lacinia eget. Donec sed tempor elit, eu tempus augue. Duis ac elit justo. Ut ex metus, semper a ultricies congue, tincidunt tristique leo. Ut porta sodales
            fermentum. Vivamus et ante ullamcorper, porta sem at, luctus mauris. Ut fermentum velit quis orci eleifend, sed auctor elit tincidunt. Maecenas in magna et augue tristique cursus. Duis consectetur vel neque a fermentum.”
          </div>
          <div class="reviewer">Jim Jones</div>
          <div class="jobrole">Bad A-- Hombre!</div>
        </div>
        <div class="testimonial active">
          <div class="image-wrap" style="background-color: #00f;">&nbsp;</div>
          <div class="testimonial-content">“I'm verbose!”</div>
          <div class="reviewer">Bob Smith</div>
        </div>
      </div>
      <!-- Testimonial container -->
      <!-- dots/circles -->
      <div class="testimonial-dots">
        <span class="testimonial-dot active"></span>
        <span class="testimonial-dot"></span>
      </div>
    </div>