Advertisement
  1. Code
  2. Animation
Code

Create the Perfect Carousel, Part 2

by
Difficulty:AdvancedLength:MediumLanguages:
This post is part of a series called Create the Perfect Carousel.
Create the Perfect Carousel, Part 1
Create the Perfect Carousel, Part 3

Welcome back to the Create the Perfect Carousel tutorial series. We're making an accessible and delightful carousel using JavaScript and Popmotion's physics, tween and input tracking capabilities.

In part 1 of our tutorial, we took a look at how Amazon and Netflix have created their carousels and evaluated the pros and cons of their approaches. With our learnings, we decided on a strategy for our carousel and implemented touch scrolling using physics.

In part 2, we're going to implement horizontal mouse scroll. We're also going to look at some common pagination techniques and implement one. Finally, we're going to hook up a progress bar that will indicate how far through the carousel the user is.

You can restore your save point by opening this CodePen, which picks up where we left off.

Horizontal Mouse Scroll

It's rare that a JavaScript carousel respects horizontal mouse scroll. This is a shame: on laptops and mice that implement momentum-based horizontal scrolling, this is by far the quickest way to navigate the carousel. It's as bad as forcing touch users to navigate via buttons rather than swipe.

Luckily, it can be implemented in just a few lines of code. At the end of your carousel function, add a new event listener:

Below your startTouchScroll event, add a stub function called onWheel:

Now, if you run the scroll wheel over the carousel and check your console panel, you'll see the wheel distance on the x-axis output.

As with touch, if wheel movement is mostly vertical, the page should scroll as usual. If it's horizontal, we want to capture the wheel movement and apply it to the carousel. So, in onWheel, replace the console.log with:

This block of code will stop page scroll if the scroll is horizontal. Updating our slider's x offset is now just a matter of taking the event's deltaX property and adding that to our current sliderX value:

We're reusing our previous clampXOffset function to wrap this calculation and make sure the carousel doesn't scroll beyond its measured boundaries.

An Aside on Throttling Scroll Events

Any good tutorial that deals with input events will explain how important it is to throttle those events. This is because scroll, mouse and touch events can all fire faster than the device's frame rate.

You don't want to perform unnecessary resource-intensive work like rendering the carousel twice in one frame, as it's a waste of resources and a quick way to make a sluggish-feeling interface.

This tutorial hasn't touched on that because the renderers provided by Popmotion implement Framesync, a tiny frame-synced job scheduler. This means you could call (v) => sliderRenderer.set('x', v) multiple times in a row, and the expensive rendering would only happen once, on the next frame.

Pagination

That's scrolling finished. Now we need to inject some life into the hitherto unloved navigation buttons.

Now, this tutorial is about interaction, so feel free to design these buttons as you wish. Personally, I find direction arrows more intuitive (and fully internationalised by default!).

How Should Pagination Work?

There are two clear strategies we could take when paginating the carousel: item-by-item or first obscured item. There's only one correct strategy but, because I've seen the other one implemented so often, I thought it'd be worth explaining why it's incorrect.

1. Item by Item

Item By Item Example

Simply measure the x offset of the next item in the list and animate the shelf by that amount. It's a very simple algorithm that I assume is picked for its simplicity rather than its user-friendliness.

The problem is that most screens will be able to show lots of items at a time, and people will scan them all before trying to navigate.

It feels sluggish, if not outright frustrating. The only situation in which this would be a good choice is if you know the items in your carousel are the same width or only slightly smaller than the viewable area.

However, if we're looking at multiple items, we're better using the first obscured item method:

2. First Obscured Item

The First Obscured Item

This method simply looks for the first obscured item in the direction we want to move the carousel, takes its x offset, and then scrolls to that.

In doing so, we pull in the maximum number of new items working on the assumption that the user has seen all those currently present.

Because we're pulling in more items, the carousel requires fewer clicks to navigate around. Faster navigation will increase engagement and ensure your users see more of your products.

Event Listeners

First, let's set up our event listeners so we can start playing around with the pagination.

We first need to select our previous and next buttons. At the top of the carousel function, add:

Then, at the bottom of the carousel function, add the event listeners:

Finally, just above your block of event listeners, add the actual functions:

goto is the function that's going to handle all the logic for pagination. It simply takes a number which represents the direction of travel we wish to paginate. gotoNext and gotoPrev simply call this function with 1 or -1, respectively.

Calculating a "Page"

A user can freely scroll this carousel, and there are n items within it, and the carousel might be resized. So the concept of a traditional page is not directly applicable here. We won't be counting the number of pages.

Instead, when the goto function is called, we're going to look in the direction of delta and find the first partially obscured item. That will become the first item on our next "page".

The first step is to get the current x offset of our slider, and use that with the full visible width of the slider to calculate an "ideal" offset to which we'd like to scroll. The ideal offset is what we would scroll to if we were naive to the contents of the slider. It provides a nice spot for us to start searching for our first item.

We can use a cheeky optimisation here. By providing our targetX to the clampXOffset function we made in the previous tutorial, we can see if its output is different to targetX. If it is, it means our targetX is outside of our scrollable bounds, so we don't need to figure out the closest item. We just scroll to the end.

Finding the Closest Item

It's important to note that the following code works on the assumption that all of the items in your carousel are the same size. Under that assumption, we can make optimisations like not having to measure the size of every item. If your items are different sizes, this will still make a good starting point. 

Above your goto function, add the findClosestItemOffset function referenced in the last snippet:

First, we need to know how wide our items are and the spacing between them. The Element.getBoundingClientRect() method can provide all the information we need. For width, we simply measure the first item element. To calculate the spacing between items, we can measure the right offset of the first item and the left offset of the second, and then subtract the former from the latter: 

Now, with the targetX and delta variables we passed through to the function, we have all the data we need to quickly calculate an offset to scroll to.

The calculation is to divide the absolute targetX value by the width + spacing. This will give us the exact number of items we can fit inside that distance.

Then, round up or down depending on the direction of pagination (our delta). This will give us the number of complete items we can fit.

Finally, multiply that number by width + spacing to give us an offset flush with a full item.

Animate the Pagination

Now that we've got our targetX calculated, we can animate to it! For this, we're going to use the workhorse of web animation, the tween.

For the uninitiated, "tween" is short for between. A tween changes from one value to another, over a set duration of time. If you've used CSS transitions, this is the same thing. 

There are a number of benefits (and shortcomings!) to using JavaScript over CSS for tweens. In this instance, because we're also animating sliderX with physics and user input, it will be easier for us to stay in this workflow for the tween.

It also means that later on we can hook up a progress bar and it'll work naturally with all our animations, for free.

We first want to import tween from Popmotion:

At the end of our goto function, we can add our tween that animates from currentX to targetX:

By default, Popmotion sets duration to 300 milliseconds and ease to easing.easeOut. These have been picked specifically to provide a responsive feel to animations that respond to user input, but feel free to play around and see if you come up with something that better fits the feel of your brand.

Progress Indicator

It's useful for users to have some indication about where in the carousel they are. For this, we can hook up a progress indicator.

Your progress bar could be styled in a number of ways. For this tutorial, we've made a coloured div, 5px high, that runs between the previous and next buttons. It's the way that we hook this up to our code and animate the bar that is important and is the focus of this tutorial.

You haven't seen the indicator yet because we originally styled it with transform: scaleX(0). We use a scale transform to adjust the width of the bar because, as we explained in part 1, transforms are more performant than changing properties like left or, in this case, width.

It also allows us to easily write code that sets the scale as a percentage: the current value of sliderX between minXOffset and maxXOffset.

Let's start by selecting our div.progress-bar after our previousButton selector:

After we define sliderRenderer, we can add a renderer for progressBar:

Now let's define a function to update the scaleX of the progress bar.

We'll use a calc function called getProgressFromValue. This takes a range, in our case min and maxXOffset, and a third number. It returns the progress, a number between 0 and 1, of that third number within the given range.

We've written the range here as maxXOffset, minXOffset when, intuitively, it should be reversed. This is because x is a negative value, and maxXOffset is also a negative value whereas minXOffset is 0. The 0 is technically the larger of the two numbers, but the smaller value actually represents the maximum offset. Negatives, huh?

We want the progress indicator to update in lockstep with sliderX, so let's change this line:

To this line:

Now, whenever sliderX updates, so will the progress bar.

Conclusion

That's it for this instalment! You can grab the latest code on this CodePen. We've successfully introduced horizontal wheel scrolling, pagination, and a progress bar.

The carousel is in pretty good shape so far! In the final instalment, we're going to take it a step further. We'll make the carousel fully keyboard accessible to ensure anyone can use it. 

We're also going to add a couple of delightful touches using a spring-powered tug when a user tries to scroll the carousel past its boundaries using either touch scroll or pagination. 

See you then!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.