Grow your CSS skills. Land your dream job.

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="http://farm8.staticflickr.com/7347/8731666710_34d07e709e_z.jpg" /></div>
        74
      </div>
      <div class="slide-wrapper">
        <div class="slide"><img class="slide-image" src="http://farm8.staticflickr.com/7384/8730654121_05bca33388_z.jpg" /></div>
        64
      </div>
      <div class="slide-wrapper">
        <div class="slide"><img class="slide-image" src="http://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.

Comments

  1. I tried on my Nexus 7 tablet and the slider doesn’t work at all. I know I have big fingers but something is going wrong with this…

    • I actually didn’t test on any Android devices for this example. I’ll try and track down a Nexus 7 and see why it’s not working.

    • Here’s a picture of it working on a Kindle Fire: http://cl.ly/PdBK It gets a little “stuck” , consistently on each slide movement, but it does change slides and animate the transition.

      I’m trying to book up my Nexus 7 tablet to check. But it won’t boot of course. omg I hate it. But I’ll check in like 72 hours when it has enough juice to start.

      Also works great on Chrome on iOS – but that’s not much of a test since it’s same as Safari on iOS (mostly, anyway, I think).

    • My best guess is because I’m using 3d transforms, which only work in the latest Android browsers. 2d transforms would be better supported but wouldn’t be as smooth.

      http://caniuse.com/transforms2d

      http://caniuse.com/transforms3d

    • On Nexus One with latest Chrome browser doesn’t work as expected. If I tap it the slide moves on, I can also pull about 15% of the next slide along and then tap it to get that slide.

      Comes closer to working on same phone with Firefox, in this case only a third of the image shown then when you press it the whole image appears and you can swipe it beautifully, take your finger off and two thirds of the image disappears

      Same phone with Dolphin browser works perfectly …

      Works perfectly on Chome on my iPad3 – nice slider!

      Hope you can debug it for Android

    • Edit – Nexus 4 … it’s been a long week. ;-O

    • Jeanine
      Permalink to comment#

      How can I change the number of images shown? I have 10 images and only 3 show. Please help!

  2. Love it ! Great job Kevin, works great on my iphone.

  3. Dominic
    Permalink to comment#

    Doesn’t work on mobile Chrome (Samsung Galaxy S4). I can only “scroll” a few pixels to the left and that’s it.

  4. I found this random plugin for this sort of thing and it has worked really well for our needs and is super smooth: http://uraniumjs.com/widgets/carousel.html

  5. nate
    Permalink to comment#

    Barely works on Google nexus 4. It looks really good when the slides actually slide.

  6. Permalink to comment#

    works quite nice on the iphone. have to try it on other devices/browser :-). the only problem with this slider for me is that it is not horizontally locked. I have this problem with most of the touch friendly sliders is used

  7. Great post! I tried it on an Android tablet, and it worked -slowly- only with flicks. On the other hand, it worked great on iOS.

    I think that multitouch events on Android are just slow: I asked this question on StackOverflow some time ago and decided to go native because of performance.

  8. Cool example.

    Using Google Chrome on a Galaxy S3 (Android 4.1.2) my initial instinct was to swipe the images which doesn’t work. On tap of an image the slide effect is great.

  9. Inigo
    Permalink to comment#

    How about jquery?

  10. No intent to spam, just want to re-confirm the above-posted observations. I tested in Firefox and Chrome on Android 4, and it’s broken i the ways described above.

    @Kevin If you can’t make it work for Android, consider adding a disclaimer section at the beginning of the post, where you’d describe why it fails on Android.

  11. Codepen doesnt work well on my N4

  12. DeeL
    Permalink to comment#

    On my Windows Phone 7.8 (HTC HD7) it doesn’t even display whole first picture. Scrolling not working at all… totally nothing… sorry. Guess it is this shitty IE browser :D

  13. Jon Koops
    Permalink to comment#

    This isn’t woking on both my Nexus 4 using Chrome and Windows Phone 8 on my HTC X8.

  14. Tried with native android browser and chrome on HTC One X, not good, the right side showed a hint of another pic, about 2mm appears when I touch. That’s all.
    Same on Samsung Galaxy S 2 native browser

  15. I will fork it and see what’s wrong

  16. rerrol

    Interesting, to be sure. Slightly glitchy on iphone4/ios 6.1.3, with image+1 showing at the right-edge,
    and a 2-finger resize triggers a scroll to end of stack. Let’s just blame 6.1.3. :-)

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".