9

I am trying to create a vertical carousel using vanilla JavaScript and CSS. I know that jQuery has a carousel library but I want to have a go at building this from scratch using no external libraries. I started off by just trying to move the top image and then I planned to move on to making the next image move. I got stuck on the first image. This is where I need your help, StackOverflowers.

My HTML:

<div class="slider vertical" >
    <img class="first opened" src="http://malsup.github.io/images/beach1.jpg">
    <img class="opened" src="http://malsup.github.io/images/beach2.jpg">
    <img src="http://malsup.github.io/images/beach3.jpg">
    <img src="http://malsup.github.io/images/beach4.jpg">
    <img src="http://malsup.github.io/images/beach5.jpg">
    <img src="http://malsup.github.io/images/beach9.jpg">
</div>
<div class="center">
    <button id="prev">∧ Prev</button>
    <button id="next">∨ Next</button>
</div>

JavaScript:

var next = document.getElementById('next');
var target = document.querySelector('.first');

next.addEventListener('click', nextImg, false);

function nextImg(){
     if (target.classList.contains('opened')) {
        target.classList.remove('opened');
        target.classList.add('closed');
    } else {
        target.classList.remove('closed');
        target.classList.add('opened');
    }
}

CSS:

div.vertical {
    width: 100px;
}

.slider {
    position: relative;
    overflow: hidden;
    height: 250px;

        -webkit-box-sizing:border-box;
       -moz-box-sizing:border-box;
        -ms-box-sizing:border-box;
            box-sizing:border-box;

    -webkit-transition:-webkit-transform 1.3s ease;
       -moz-transition:   -moz-transform 1.3s ease;
        -ms-transition:    -ms-transform 1.3s ease;
            transition:        transform 1.3s ease;
}

.slider img {
    width: 100px;
    height: auto;
    padding: 2px;
}

.first.closed{
    /* partially offscreen */
    -webkit-transform: translate(0, -80%);
       -moz-transform: translate(0, -80%);
        -ms-transform: translate(0, -80%);
            transform: translate(0, -80%);
}

.first.opened{
    /* visible */
    -webkit-transform: translate(0, 0%);
       -moz-transform: translate(0, 0%);
        -ms-transform: translate(0, 0%);
            transform: translate(0, 0%);
}

My mode of thinking was:

  1. use classes to move and show content
  2. use JavaScript to add and remove classes

I think I may not have broken the problem down properly.

This is how I would like it to look: http://jsfiddle.net/natnaydenova/7uXPx/

And this is my abysmal attempt: http://jsfiddle.net/6cb58pkr/

5
  • ux.stackexchange.com/questions/10312/are-carousels-effective Commented Aug 18, 2015 at 18:30
  • Incidentally, in your JS Fiddle: don't confuse the syntax of getElementById('<id>') with that of querySelector('#<id>'); otherwise you get the message (in the console): Uncaught TypeError: Cannot read property 'addEventListener' of null. I've corrected that error and updated the demo: jsfiddle.net/davidThomas/6cb58pkr/1 Commented Aug 18, 2015 at 18:35
  • Thank you @David Thomas that was it! Must have been sleeping when doing this. Commented Aug 18, 2015 at 18:39
  • Most if not all of the major css aspects that are required for such an implementation, I think you have already included. (Especially fixed heights and widths, hidden overflow). One thing I would say though is that perhaps your javascript method should also be responsible for viewing/scrolling position within the list - this helps you to support any number of images, with any dimensions. Ive recently been using css3 flex too. May help with layout? Sorry its a bit vague advice. Good luck. :) Commented Aug 18, 2015 at 18:43
  • Thanks for the advice @ne1410s I will have a sleep on this and work on a better solution that can move more than the first image. Commented Aug 18, 2015 at 18:45

1 Answer 1

19

An alternative to using CSS transform properties is to give the carousel absolute positioning inside a wrapper div and manipulate the carousel's top property. Then you can use any easing function you like to animate the sliding motion. In the snippet below, I use cubic easing in/out.

A tricky thing to watch out for is the order in which you rotate the images and perform the sliding animation. When you want to show the next picture below, you have to:

  • slide the carousel up by the height of one picture frame
  • rotate the first image to the end
  • reset the carousel's vertical offset to zero

To show the next picture above:

  • rotate the last image to the beginning
  • instantly move the carousel up by the height of one picture frame
  • slide the carousel down until its vertical offset reaches zero

In the following snippet, you can set the width of the carousel by adjusting Carousel.width at the top of the script. (Although the image height doesn't have to be the same as the image width, I do assume that all images have the same dimensions.) You can also play around with the Carousel.numVisible and Carousel.duration parameters.

var Carousel = {
  width: 100,     // Images are forced into a width of this many pixels.
  numVisible: 2,  // The number of images visible at once.
  duration: 600,  // Animation duration in milliseconds.
  padding: 2      // Vertical padding around each image, in pixels.
};

function rotateForward() {
  var carousel = Carousel.carousel,
      children = carousel.children,
      firstChild = children[0],
      lastChild = children[children.length - 1];
  carousel.insertBefore(lastChild, firstChild);
}
function rotateBackward() {
  var carousel = Carousel.carousel,
      children = carousel.children,
      firstChild = children[0],
      lastChild = children[children.length - 1];
  carousel.insertBefore(firstChild, lastChild.nextSibling);
}

function animate(begin, end, finalTask) {
  var wrapper = Carousel.wrapper,
      carousel = Carousel.carousel,
      change = end - begin,
      duration = Carousel.duration,
      startTime = Date.now();
  carousel.style.top = begin + 'px';
  var animateInterval = window.setInterval(function () {
    var t = Date.now() - startTime;
    if (t >= duration) {
      window.clearInterval(animateInterval);
      finalTask();
      return;
    }
    t /= (duration / 2);
    var top = begin + (t < 1 ? change / 2 * Math.pow(t, 3) :
                               change / 2 * (Math.pow(t - 2, 3) + 2));
    carousel.style.top = top + 'px';
  }, 1000 / 60);
}

window.onload = function () {
  document.getElementById('spinner').style.display = 'none';
  var carousel = Carousel.carousel = document.getElementById('carousel'),
      images = carousel.getElementsByTagName('img'),
      numImages = images.length,
      imageWidth = Carousel.width,
      aspectRatio = images[0].width / images[0].height,
      imageHeight = imageWidth / aspectRatio,
      padding = Carousel.padding,
      rowHeight = Carousel.rowHeight = imageHeight + 2 * padding;
  carousel.style.width = imageWidth + 'px';
  for (var i = 0; i < numImages; ++i) {
    var image = images[i],
        frame = document.createElement('div');
    frame.className = 'pictureFrame';
    var aspectRatio = image.offsetWidth / image.offsetHeight;
    image.style.width = frame.style.width = imageWidth + 'px';
    image.style.height = imageHeight + 'px';
    image.style.paddingTop = padding + 'px';
    image.style.paddingBottom = padding + 'px';
    frame.style.height = rowHeight + 'px';
    carousel.insertBefore(frame, image);
    frame.appendChild(image);
  }
  Carousel.rowHeight = carousel.getElementsByTagName('div')[0].offsetHeight;
  carousel.style.height = Carousel.numVisible * Carousel.rowHeight + 'px';
  carousel.style.visibility = 'visible';
  var wrapper = Carousel.wrapper = document.createElement('div');
  wrapper.id = 'carouselWrapper';
  wrapper.style.width = carousel.offsetWidth + 'px';
  wrapper.style.height = carousel.offsetHeight + 'px';
  carousel.parentNode.insertBefore(wrapper, carousel);
  wrapper.appendChild(carousel);
  var prevButton = document.getElementById('prev'),
      nextButton = document.getElementById('next');
  prevButton.onclick = function () {
    prevButton.disabled = nextButton.disabled = true;
    rotateForward();
    animate(-Carousel.rowHeight, 0, function () {
      carousel.style.top = '0';
      prevButton.disabled = nextButton.disabled = false;
    });
  };
  nextButton.onclick = function () {
    prevButton.disabled = nextButton.disabled = true;
    animate(0, -Carousel.rowHeight, function () {
      rotateBackward();
      carousel.style.top = '0';
      prevButton.disabled = nextButton.disabled = false;
    });
  };
};
body {
  font-family: sans-serif;
}
.buttons {
  margin: 5px 0;
}
button {
  font-size: 14px;
  display: inline;
  padding: 3px 6px;
  border: 2px solid #ccc;
  background: #fff;
  border-radius: 5px;
  outline: none;
}
button:hover {
  border: 2px solid #888;
  background: #ffe;
  cursor: pointer;
}
#carouselWrapper {
  position: relative;
  overflow: hidden;
}
#carousel {
  position: absolute;
  visibility: hidden;
}
<div id="spinner"> 
  Loading...
</div>

<div id="carousel">
  <img src="http://malsup.github.io/images/beach1.jpg">
  <img src="http://malsup.github.io/images/beach2.jpg">
  <img src="http://malsup.github.io/images/beach3.jpg">
  <img src="http://malsup.github.io/images/beach4.jpg">
  <img src="http://malsup.github.io/images/beach5.jpg">
  <img src="http://malsup.github.io/images/beach9.jpg">
</div>

<div class="buttons">
  <button id="prev">&uarr; Prev</button>
  <button id="next">&darr; Next</button>
</div>

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you @Michael Laszlo for an elegant solution and explaining your thought process, I think that's the most valuable part. Its all working now.
@Michael How can I make your code stop if I do not want loop?
@NikhilKapoor You can add an index property to the carousel object. You would have to increment it in rotateForward and decrement it in rotateBackward. Then, at the start of these functions, you could check the index and return early if the index is at the limit.
@Michael Thanks I am using your code with angular and it is showing white space then suddenly picture comes. Is there any way we can chat?
@Michael how to keep the prev disabled when the image is at first and next disabled when we reach the last image

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.