The JavaScript Behind Touch-Friendly Sliders

Published by Guest Author

The following is a guest post by Kevin Foley. Kevin is a developer at Squarespace doing cool stuff with their developer platform, among other things. He was recently working on a swipeable image gallery and he agreed to share some of that work here!

A few weeks ago Chris posted a tutorial for creating a Slider with Sliding Background Images. Around the same time I was working on some new swipeable galleries, so Chris suggested I write up a tutorial for how to add swipe support to his slider. Here it is!

When creating a swipeable gallery there are two techniques — that I know of — you can choose from. You can animate the scroll position, or move the element using translate. There are pros and cons to each.

Using Translate

Moving the slider with translate gives you the advantage of hardware acceleration and subpixel animation. However, on the initial touch event you might notice a small delay — only a few tens of milliseconds — before the slider starts moving. This isn’t very well documented, I’ve just noticed it in my experience.

Using Overflow Scroll

Overflow scroll is extremely responsive to the initial touch because it’s native to the browser. You don’t have to wait for the event listener in JavaScript. But you lose out on all the smoothness of moving elements with translate.

For this tutorial we’re going to use translate because I think it looks better.

The HTML

The HTML in this example is going to differ from Chris’s original example. Instead of setting the image as a background image, we’re going to set it as an element. That will allow us to move the image to gain that cool panning effect using translate instead of animating the background position.

<div class="slider-wrap">
  <div class="slider" id="slider">
    <div class="holder">
      <div class="slide-wrapper">
        <div class="slide"><img class="slide-image" src="//farm8.staticflickr.com/7347/8731666710_34d07e709e_z.jpg" /></div>
        74
      </div>
      <div class="slide-wrapper">
        <div class="slide"><img class="slide-image" src="//farm8.staticflickr.com/7384/8730654121_05bca33388_z.jpg" /></div>
        64
      </div>
      <div class="slide-wrapper">
        <div class="slide"><img class="slide-image" src="//farm8.staticflickr.com/7382/8732044638_9337082fc6_z.jpg" /></div>
        82
      </div>
    </div>
  </div>
</div>

The CSS

For the most part the CSS is the same as Chris’ so I won’t rehash how to set up the layout. But there are a few key differences.

Instead of simply adding overflow scroll, we need to animate the slides. So for that we’re going to use a class to set up the transition and add it with JavaScript when we need it.

.animate { transition: transform 0.3s ease-out; }

IE 10 handles touch events differently than mobile Webkit browsers, like Chrome and Safari. We’ll address the Webkit touch events in JavaScript, but in IE10 we can create this entire slider (almost) with nothing but CSS.

.ms-touch.slider {
  overflow-x: scroll;
  overflow-y: hidden;
  
  -ms-overflow-style: none;
  /* Hides the scrollbar. */
  
  -ms-scroll-chaining: none;
  /* Prevents Metro from swiping to the next tab or app. */
  
  -ms-scroll-snap-type: mandatory;
  /* Forces a snap scroll behavior on your images. */
  
  -ms-scroll-snap-points-x: snapInterval(0%, 100%);
  /* Defines the y and x intervals to snap to when scrolling. */
}

Since these properties are probably new to most people (they were to me, too) I’ll walk through each one and what it does.

-ms-scroll-chaining

The Surface tablet switches browser tabs when you swipe across the page, rendering all swipe events useless for developers. Luckily it’s really easy to override that behavior by setting scroll chaining to none on any given element.

-ms-scroll-snap-type

When set to mandatory, this property overrides the browser’s default scrolling behavior and forces a scrollable element to snap to a certain interval.

-ms-scroll-snap-points-x

This property sets the intervals the scrollable element will snap to. It accepts two numbers: the first number is the starting point; the second is the snap interval. In this example, each slide is the full width of the parent element, which means the interval should be 100% — i.e. the element will snap to 100%, 200%, 300%, and so on.

-ms-overflow-style

This property lets you hide the scrollbar when you set it to none.

The JavaScript

The first thing we have to do in JavaScript is detect what kind of touch device we’re using. IE 10 uses pointer events while Webkit has “touchstart,” “touchmove,” and “touchend.” Since the IE 10 slider is (almost) all in CSS we need to detect that and add a class to the wrapper.

if (navigator.msMaxTouchPoints) {
  $('#slider').addClass('ms-touch');
}

Pretty simple. If you tested the slider at this point it would be a functioning swipeable slideshow. But we still need to add the panning effect on the images.

if (navigator.msMaxTouchPoints) {
  $('#slider').addClass('ms-touch');

  // Listed for the scroll event and move the image with translate.
  $('#slider').on('scroll', function() {
    $('.slide-image').css('transform','translate3d(-' + (100-$(this).scrollLeft()/6) + 'px,0,0)');
  });
}

And that’s it for IE 10.

Now for the webkit way. This will all be wrapped in the else statement. First we’ve just got to define a few variables.

else {
  var slider = {

    // The elements.
    el: {
      slider: $("#slider"),
      holder: $(".holder"),
      imgSlide: $(".slide-image")
    },

    // The stuff that makes the slider work.
    slideWidth: $('#slider').width(), // Calculate the slider width.

    // Define these as global variables so we can use them across the entire script.
    touchstartx: undefined,
    touchmovex: undefined, 
    movex: undefined,
    index: 0,
    longTouch: undefined,
    // etc

Then we need to initialize the function and define the events.

    // continued

    init: function() {
      this.bindUIEvents();
    },

    bindUIEvents: function() {

      this.el.holder.on("touchstart", function(event) {
        slider.start(event);
      });

      this.el.holder.on("touchmove", function(event) {
        slider.move(event);
      });

      this.el.holder.on("touchend", function(event) {
        slider.end(event);
      });

    },

Now for the fun stuff that actually makes stuff happen when you swipe the slider.

Touchstart

On the iPhone (and most other touch sliders) if you move the slider slowly, just a little bit, it will snap back into its original position. But if you do it quickly it will increment to the next slide. That fast movement is called a flick. And there isn’t any native way to test for a flick so we’ve got to use a little hack. On touchstart we intitialize a setTimeout function and set a variable after a certain amount of time.

this.longTouch = false;
setTimeout(function() {
  // Since the root of setTimout is window we can’t reference this. That’s why this variable says window.slider in front of it.
  window.slider.longTouch = true;
}, 250);

We’ve also got to get the original position of the touch to make out animation work. If you’ve never done this before, it’s a little strange. JavaScript lets you define multitouch events, so you can pass the touches event a number that represents the amount of fingers you’re listening for. For this example I’m really only interested in one finger/thumb so in the code sample below that’s what the [0] is for.

// Get the original touch position.
this.touchstartx =  event.originalEvent.touches[0].pageX;

Before we start moving the slider I’m going to remove the animate class. I know there is no animate class on the elements right now, but we need this for the subsequent slides. We’ll add it back to an element on touchend.

$('.animate').removeClass('animate');

Touchmove

The touchmove event behaves similarly to scroll events in JavaScript. That is, if you do something on scroll it’s going to execute a bunch of times while the scroll is occuring. So we’re going to get the touchmove position continuously while the finger/thumb is moving.

// Continuously return touch position.
this.touchmovex =  event.originalEvent.touches[0].pageX;

Then we’ll do a quick calculation using the touchstart position we got in the last event and the touchmove position to figure out how to translate the slide.

// Calculate distance to translate holder.
this.movex = this.index*this.slideWidth + (this.touchstartx - this.touchmovex);

Then we need to pan the image, like Chris did in the original example. We’re going to use the same magic numbers he did.

// Defines the speed the images should move at.
var panx = 100-this.movex/6;

Now we need to add in some logic to handle edge cases. If you’re on the first slide or the last slide this logic will stop the image panning if you’re scrolling in the wrong direction (i.e. toward no content). This might not be the best way to handle this, but it works for me right now.

if (this.movex < 600) { // Makes the holder stop moving when there is no more content.
  this.el.holder.css('transform','translate3d(-' + this.movex + 'px,0,0)');
}
if (panx < 100) { // Corrects an edge-case problem where the background image moves without the container moving.
  this.el.imgSlide.css('transform','translate3d(-' + panx + 'px,0,0)');
 }

Touchend

In the touchend event we’ve got to figure out how far the user moved the slide, at what speed, and whether or not that action should increment to the next slide.

First we need to see exactly how far the distance of the swipe was. We’re going to calculate the absolute value of the distance moved to see if the user swiped.

// Calculate the distance swiped.
var absMove = Math.abs(this.index*this.slideWidth - this.movex);

Now we’re going to figure out if the slider should increment. All the other calculations in this example are based on the index variable, so this is the real logic behind the script. It checks if the user swiped the minimum distance to increment the slider, or if the movement was a flick. And if it meets either of those two criteria, which direction did the swipe go in.

// Calculate the index. All other calculations are based on the index.
if (absMove > this.slideWidth/2 || this.longTouch === false) {
  if (this.movex > this.index*this.slideWidth && this.index < 2) {
    this.index++;
  } else if (this.movex < this.index*this.slideWidth && this.index > 0) {
    this.index--;
  }
}

Now we add the animate class and set the new translate position.

// Move and animate the elements.
this.el.holder.addClass('animate').css('transform', 'translate3d(-' + this.index*this.slideWidth + 'px,0,0)');
this.el.imgSlide.addClass('animate').css('transform', 'translate3d(-' + 100-this.index*50 + 'px,0,0)');

The Conclusion

So, yea, hooray for IE 10.

But seriously, creating swipeable galleries is kind of a pain, considering it’s such a common idiom on mobile OS’s. And the end result won’t be as good as native swiping. But it will be close.

Here's the complete demo:

Check out this Pen!

But you're best bet is checking it out on your touch device here.