Advertisement
  1. Code
  2. JavaScript
Code

Create the Perfect Carousel, Part 1

by
Difficulty:AdvancedLength:LongLanguages:
This post is part of a series called Create the Perfect Carousel.
Create the Perfect Carousel, Part 2

Carousels are a staple of streaming and e-commerce sites. Both Amazon and Netflix use them as prominent navigation tools. In this tutorial, we'll evaluate the interaction design of both, and use our findings to implement the perfect carousel.

In this tutorial series, we'll also be learning some functions of Popmotion, a JavaScript motion engine. It offers animation tools like tweens (useful for pagination), pointer tracking (for scrolling), and spring physics (for our delightful finishing touches.)

Part 1 will evaluate how Amazon and Netflix have implemented scrolling. We'll then implement a carousel that can be scrolled via touch.

By the end of this series, we'll have implemented wheel and touchpad scroll, pagination, progress bars, keyboard navigation, and some little touches using spring physics. We'll also have been exposed to some basic functional composition.

Perfect?

What does it take for a carousel to be "perfect"? It has to be accessible by:

  • Mouse: It should offer previous and next buttons that are easy to click and don't obscure content.
  • Touch: It should track the finger, and then scroll with the same momentum as when the finger lifts from the screen.
  • Scroll wheel: Often overlooked, the Apple Magic Mouse and many laptop trackpads offer smooth horizontal scrolling. We should utilise those capabilities!
  • Keyboard: Many users prefer not to, or are unable to use a mouse for navigation. It's important we make our carousel accessible so those users can use our product too.

Finally, we'll take things that extra step further and make this a confident, delightful piece of UX by making the carousel respond clearly and viscerally with spring physics when the slider has reached the end.

Setup

First, let's get the HTML and CSS necessary to build a rudimentary carousel by forking this CodePen

The Pen is set up with Sass for preprocessing CSS and Babel for transpiling ES6 JavaScript. I've also included Popmotion, which can be accessed with window.popmotion.

You can copy the code to a local project if you prefer, but you'll need to ensure your environment supports Sass and ES6. You'll also need to install Popmotion with npm install popmotion.

Creating a New Carousel

On any given page, we might have many carousels. So we need a method to encapsulate the state and functionality of each.

I'm going to use a factory function rather than a class. Factory functions avoid the need to use the often-confusing this keyword and will simplify the code for the purposes of this tutorial.

In your JavaScript editor, add this simple function:

We'll be adding our carousel-specific code inside this carousel function.

The Hows and Whys of Scrolling

Our first task is to make the carousel scroll. There are two ways we could go about this:

Native Browser Scrolling

The obvious solution would be to set overflow-x: scroll on the slider. This would allow native scrolling on all browsers, including touch and horizontal mouse wheel devices.

There are, however, drawbacks to this approach:

  • Content outside the container would not be visible, which can be restrictive for our design.
  • It also limits the ways we can use animations to indicate we've reached the end.
  • Desktop browsers will have an ugly (though accessible!) horizontal scroll bar.

Alternatively:

Animate translateX

We could also animate the carousel's translateX property. This would be very versatile as we'd be able to implement exactly the design we like. translateX is also very performant, as unlike the CSS left property it can be handled by the device's GPU.

On the downside, we'd have to reimplement scrolling functionality using JavaScript. That's more work, more code.

How Do Amazon and Netflix Approach Scrolling?

Both Amazon and Netflix carousels make different trade-offs in approaching this problem.

Amazon animates the carousel's left property when in "desktop" mode. Animating left is an incredibly poor choice, as changing it triggers a layout recalculation. This is CPU-intensive, and older machines will struggle to hit 60fps.

Whoever made the decision to animate left instead of translateX must be a real idiot (spoiler: it was me, back in 2012. We weren't as enlightened in those days.)

When it detects a touch device, the carousel uses the browser's native scrolling. The problem with only enabling this in "mobile" mode is desktop users with horizontal scroll wheels miss out. It also means any content outside the carousel will have to be visually cut off:

Screenshot of Amazon illustrating lack of design bleed

Netflix correctly animates the carousel's translateX property, and it does so on all devices. This enables them to have a design that bleeds outside the carousel:

Screenshot of Netflix carousel illustrating design bleed

This, in turn, allows them to make a fancy design where items are enlarged outside of the x and y edges of the carousel and the surrounding items move out of their way:

Screenshot of Netflix carousel illustrating enlarged item

Unfortunately, Netflix's reimplementation of scrolling for touch devices is unsatisfactory: it uses a gesture-based pagination system which feels slow and cumbersome. There's also no consideration for horizontal scroll wheels.

We can do better. Let's code!

Scrolling Like a Pro

Our first move is to grab the .slider node. While we're at it, let's grab the items it contains so we can figure out the slider's dimension.

Measuring the Carousel

We can figure out the visible area of the slider by measuring its width:

We'll also want the total width of all the items contained within. To keep our carousel function relatively clean, let's put this calculation in a separate function at the top of our file.

By using getBoundingClientRect to measure the left offset of our first item and the right offset of our last item, we can use the difference between them to find the total width of all items.

After our sliderVisibleWidth measurement, write:

We can now figure out the maximum distance our carousel should be allowed to scroll. It's the total width of all our items, minus one full width of our visible slider. This provides a number that allows the rightmost item to align with the right of our slider:

With these measurements in place, we're ready to start scrolling our carousel.

Setting translateX

Popmotion comes with a CSS renderer for the simple and performant setting of CSS properties. It also comes with a value function which can be used to track numbers and, importantly (as we'll soon see), to query their velocity.

At the top of your JavaScript file, import them like so:

Then, on the line after we set minXOffset, create a CSS renderer for our slider:

And create a value to track our slider's x offset and update the slider's translateX property when it changes:

Now, moving the slider horizontally is as simple as writing:

Try it!

Touch Scroll

We want our carousel to start scrolling when a user drags the slider horizontally and to stop scrolling when a user stops touching the screen. Our event handlers will look like this:

In our startTouchScroll function, we want to:

  • Stop any other actions powering sliderX.
  • Find the origin touch point.
  • Listen to the next touchmove event to see if the user is dragging vertically or horizontally.

After document.addEventListener, add:

This will stop any other actions (like the physics-powered momentum scroll that we'll implement in stopTouchScroll) from moving the slider. This will allow the user to immediately "catch" the slider if it scrolls past an item or title that they want to click on.

Next, we need to store the origin touch point. That will allow us to see where the user moves their finger next. If it's a vertical movement, we'll allow the scrolling of the page as usual. If it's a horizontal movement, we'll scroll the slider instead.

We want to share this touchOrigin between event handlers. So after let action; add:

Back in our startTouchScroll handler, add:

We can now add a touchmove event listener to the document to determine the drag direction based on this touchOrigin:

Our determineDragDirection function is going to measure the next touch location, check it has actually moved and, if so, measure the angle to determine whether it's vertical or horizontal:

Popmotion includes some helpful calculators for measuring things like the distance between two x/y coordinates. We can import those like this:

Then measuring the distance between the two points is a matter of using the distance calculator:

Now if the touch has moved, we can unset this event listener.

Measure the angle between the two points with the angle calculator:

We can use this to determine whether this angle is a horizontal or vertical angle, by passing it to the following function. Add this function to the very top of our file:

This function returns true if the provided angle is within -90 +/- 45 degrees (straight up) or 90 +/-45 degrees (straight down.) So we can add another return clause if this function returns true.

Pointer Tracking

Now we know the user is trying to scroll the carousel, we can begin tracking their finger. Popmotion offers a pointer action that will output the x/y coordinates of a mouse or touch pointer.

First, import pointer:

To track the touch input, provide the originating event to pointer:

We want to measure the initial x position of our pointer and apply any movement to the slider. For that, we can use a transformer called applyOffset.

Transformers are pure functions that take a value, and return it—yes—transformed. For instance: const double = (v) => v * 2.

applyOffset is a curried function. This means that when we call it, it creates a new function that can then be passed a value. We first call it with a number we want to measure the offset from, in this case the current value of action.x, and a number to apply that offset to. In this case, that's our sliderX.

So our applyOffset function will look like this:

We can now use this function in the pointer's output callback to apply pointer movement to the slider.

Stopping, With Style

The carousel is now draggable by touch! You can test this by using device emulation in Chrome's Developer Tools.

It feels a little janky, right? You may have encountered scrolling that feels like this before: You lift your finger, and the scrolling stops dead. Or the scrolling stops dead and then a little animation takes over to fake a continuation of the scrolling.

We're not going to do that. We can use the physics action in Popmotion to take the true velocity of sliderX and apply friction to it over a duration of time.

First, add it to our ever-growing list of imports:

Then, at the end of our stopTouchScroll function, add:

Here, from and velocity are being set with the current value and velocity of sliderX. This ensures our physics simulation has the same initial starting conditions as the user's dragging motion.

friction is being set as 0.2. Friction is set as a value from 0 to 1, with 0 being no friction at all and 1 being absolute friction. Try playing around with this value to see the change it makes to the "feeling" of the carousel when a user stops dragging.

Smaller numbers will make it feel lighter, and larger numbers will make movement heavier. For a scrolling motion, I feel 0.2 hits a nice balance between erratic and sluggish.

Boundaries

But there's a problem! If you've been playing around with your new touch carousel, it's obvious. We haven't bounded movement, making it possible to literally throw your carousel away!

There's another transformer for this job, clamp. This is also a curried function, meaning if we call it with a min and max value, say 0 and 1, it will return a new function. In this example, the new function will restrict any number given to it to between 0 and 1:

First, import clamp:

We want to use this clamping function across our carousel, so add this line after we define minXOffset:

We're going to amend the two output we've set on our actions using some light functional composition with the pipe transformer.

Pipe

When we call a function, we write it like this:

If we want to give the output of that function to another function, we might write that like this:

This becomes slightly difficult to read, and it only gets worse as we add more and more functions.

With pipe, we can compose a new function out of foo and bar which we can reuse:

It's also written in a natural start -> finish order, which makes it easier to follow. We can use this to compose applyOffset and clamp into a single function. Import pipe:

Replace the output callback of our pointer with:

And replace the output callback of physics with:

This kind of functional composition can quite neatly create descriptive, step-by-step processes out of smaller, reusable functions.

Now, when you drag and throw the carousel, it won't budge outside of its boundaries.

The abrupt stop isn't very satisfying. But that's a problem for a later part!

Conclusion

That's all for part 1. So far, we've taken a look at existing carousels to see the strengths and weaknesses of different approaches to scrolling. We've used Popmotion's input tracking and physics to performantly animate our carousel's translateX with touch scrolling. We've also been introduced to functional composition and curried functions.

You can grab a commented version of the "story so far" on this CodePen.

In upcoming installments, we'll look at:

  • scrolling with a mouse wheel
  • remeasuring the carousel when the window resizes
  • pagination, with keyboard and mouse accessibility
  • delightful touches, with the help of spring physics

Look forward to seeing you there!

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.