Let’s Play With Hardware-Accelerated CSS

Advertisement

If you’re a developer of mobile Web apps, then you’ve heard this before:

Native apps perform better than Web apps.

But what does “perform better” mean? In the context above, performance is usually about measurable aspects such as loading time and responsiveness to user interaction. But more often than not, statements about performance lie within the realm of animations and transitions and how smooth they are.

How Do We Get Smoother Transitions In A Mobile Web App?

We humans tend to perceive a transition as being “smooth” when the number of frames per second (FPS) drawn on the screen is above a certain cognitive threshold — about 30 or so, arguably. And one of the things that native apps have been particularly better at than Web apps is keeping the FPS high while swiping, tapping, pinching, executing and all other such verbs that were nonexistent a little over six years ago.

This is possible because native applications can access the device’s graphical processing unit (GPU) to make pixels fly. Web applications, on the other hand, run in the context of the browser, which lets the software do most (if not all) of the rendering, resulting in less horsepower for transitions. But the Web has been catching up, and most browser vendors now provide graphical hardware acceleration by means of particular CSS rules.

Knowing this is the key to getting smoother transitions on mobile Web apps. But this is old news, you say?

For some it may be, because it has been around for a while and has found its way into respected mobile Web frameworks such as Sencha1’s line of products. But if you’re doing any custom CSS or jQuery magic, then knowing how to kick those FPS up a notch manually is useful. So, in the name of science (and some good fun), let’s get our hands dirty.

Time To Roll Up Our Sleeves

We’re going to build a fun little slideshow that…

  1. uses hardware acceleration
  2. to present five full-screen slides
  3. of kittens
  4. to swipe through
  5. with a perfect touch:pixel ratio,
  6. that snap smoothly from one slide to the next,
  7. and that work on desktops, phones and tablets
  8. in portrait and landscape mode
  9. and throws Apple’s patented rubber-band-effect in the mix as a bonus,
  10. all in under 50 lines of code.

Really? Yes. Let’s get started!

First, we’ll create a simple HTML page that provides five full-screen slides of kittens, courtesy of placekitten2, and allows you to scroll through them. The page will consist of a single #slides container with five .slide elements, each of which has a background image stretched by CSS to fully cover the slide.

<!doctype html>
<html>
   <head>
      <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
      <link href="style.css" type="text/css" rel="stylesheet" />
   </head>
   <body>
      <div id="slides">
         <div class="slide"></div>
         <div class="slide"></div>
         <div class="slide"></div>
         <div class="slide"></div>
         <div class="slide"></div>
      </div>
   </body>
</html>

Most of this is fairly straightforward.  We’re using well-known viewport meta tag3 to prevent user scaling:

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>

Note: We’ll use the -webkit- vendor prefix in the demo files just for the sake of keeping the code short, and we’ll stick to vanilla div elements and forget about user scaling to limit the scope of this article. This article does not attempt to be an example of how to make a cross-browser example for all mobile devices out there, but to illustrate the difference that GPU acceleration can make on mobile devices and—as the title suggests—“play” with it and get your coding hands dirty. In general, we should use prefixes for all browsers (at least the major ones) that support that feature (or plan to do so in near future): -moz-, -ms-, -o-, and -webkit-.

The style.css file will start with the following:

html, body {
  height: 100%;
}

body {
  margin: 0;
  overflow: visible;
  background: #333;
}

#slides {
  width: 100%; height: 100%;
  white-space: nowrap;
  font-size: 0;
  -webkit-transform: translate3d(0,0,0);
  -moz-transform: translate3d(0,0,0);
  -ms-transform: translate3d(0,0,0);
  -o-transform: translate3d(0,0,0);
  transform: translate3d(0,0,0);
}

.slide {
  width: 100%; height: 100%;
  display: inline-block;
  background-size: cover;
}

.animate {
  -webkit-transition: all .3s ease-out;
  -moz-transition: all .3s ease-out;
  -ms-transition: all .3s ease-out;
  -o-transition: all .3s ease-out;
  transition: all .3s ease-out;
}

.slide:nth-child(1) { background: url(http://placekitten.com/640/480);}
.slide:nth-child(2) { background: url(http://placekitten.com/641/480);}
.slide:nth-child(3) { background: url(http://placekitten.com/642/480);}
.slide:nth-child(4) { background: url(http://placekitten.com/643/480);}
.slide:nth-child(5) { background: url(http://placekitten.com/644/480);}

Let’s look at the #slides declaration:

#slides {
  width: 100%; height: 100%;
  white-space: nowrap;
  font-size: 0;
  -webkit-transform: translate3d(0,0,0);
  -moz-transform: translate3d(0,0,0);
  -ms-transform: translate3d(0,0,0);
  -o-transform: translate3d(0,0,0);
  transform: translate3d(0,0,0);
}

Our #slides div is set to be full screen. Because we’re using the html doctype, we also needed to make our html and body elements take up 100% of the width and height. The #slides container requires that none of its .slide children wrap, and the 0 font size removes any gaps between the slides when displayed horizontally.

The slides will be aligned like so:

The alignment of images used in our demo.
The alignment of images used in our demo.
4

However, only one slide will be visible at a time in full screen. The semi-transparent red box below represents the browser’s full screen.

Only one slide will be visible at a time.
Only one slide will be visible at a time.
5

Then we’ll use the hardware-accelerated -webkit-transform: translate3d CSS rule to “translate” the slides’ horizontal (x) position to the left or right.

The WebKit blog describes translate3d as follows6:

translate3d(x, y, z), translateZ(z)
Move the element in x, y and z, and just move the element in z. Positive z is towards the viewer. Unlike x and y, the z value cannot be a percentage.

The fun part is that WebKit also offers a simpler 2D method that does the same thing: translate(x, y). But translate3d(x, y, z) uses the GPU, and that’s what we want. For now, we’ll set it to (0,0,0):

transform: translate3d(0,0,0);

The following line makes sure that the slide’s background covers the entire element, regardless of its width or height:

background-size: cover;

The combination of our viewport meta tag that sets the page’s width to device-width and each slide being 100% in width and height makes our application work full screen on desktops, tablets and mobiles, regardless of the viewport’s initial width. For instance, if you open up the page on an iPhone and scroll just a tiny bit to the left, you’ll see that the next slide is already entering the screen from the right:

Screenshot of our app on the iPhone, scrolled just a tiny bit to the left.
Screenshot of our app on the iPhone, scrolled just a tiny bit to the left.
7

It doesn’t even matter whether you start in portrait or landscape mode (although today we won’t be adding support for adjusting to changes in orientation once the app has loaded).

To animate the slides to snap into view, we’ll define an extra class, named animate:

.animate {
  transition: all .3s ease-out;
}

This is a CSS3 transition8 that tells Webkit to animate all of its properties using the ease-out path over 300 milliseconds.

We use the CSS3 nth-child9 pseudo-class to make each slide display a different kitten in an image of the given width and height. We specify different dimensions in order to get different images because that’s how placekitten works.

.slide:nth-child(1) { background: url(http://placekitten.com/640/480);}
.slide:nth-child(2) { background: url(http://placekitten.com/641/480);}
.slide:nth-child(3) { background: url(http://placekitten.com/642/480);}
.slide:nth-child(4) { background: url(http://placekitten.com/643/480);}
.slide:nth-child(5) { background: url(http://placekitten.com/644/480);}

Let’s open this up in Chrome or Safari on a desktop. This is what you should be seeing right now:

Our Web app so far, without any code
Our Web app so far, without any code.
10

That’s right. We see only one kitten presented full screen and a large horizontal scroll bar that grants access to the other four slides. Right now, there’s no animation and no sliding other than the default scrolling behavior.

Adding The Page Slide Effect

Our approach to swiping each slide into view is for the user to hold down their finger or mouse and then move the entire #slide container, along with all of the slides in it. When the user stops moving, we let the browser animate the next (or previous) slide into view by means of a CSS transition. You can fine-tune or add to this basic set-up a lot afterwards if you like.

OK, first we need change overflow: visible to overflow: hidden in the body element’s CSS declaration to get rid of the default scrolling functionality.

body {
  margin: 0;
  overflow: hidden;
  background: #333;
}

Then, we include jQuery11 and our own script.js file just before the closing body tag12:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="script.js"></script>

Below is the entire contents of script.js. It consists of three methods: slideStart, slide and slideEnd.

$(function () {
  var sliding = startClientX = startPixelOffset = pixelOffset = currentSlide = 0,
  slideCount = $('.slide').length;

$('html').live('mousedown touchstart', slideStart);
  $('html').live('mouseup touchend', slideEnd);
  $('html').live('mousemove touchmove', slide);

function slideStart(event) {
    if (event.originalEvent.touches)
      event = event.originalEvent.touches[0];
    if (sliding == 0) {
      sliding = 1;
      startClientX = event.clientX;
    }
  }

 function slide(event) {
    event.preventDefault();
    if (event.originalEvent.touches)
      event = event.originalEvent.touches[0];
     var deltaSlide = event.clientX - startClientX;

if (sliding == 1 && deltaSlide != 0) {
      sliding = 2;
      startPixelOffset = pixelOffset;
    }

if (sliding == 2) {
      var touchPixelRatio = 1;
      if ((currentSlide == 0 && event.clientX > startClientX) ||
          (currentSlide == slideCount - 1 && event.clientX < startClientX))
        touchPixelRatio = 3;
      pixelOffset = startPixelOffset + deltaSlide / touchPixelRatio;
      $('#slides').css('transform', 'translate3d(' + pixelOffset + 'px,0,0)').removeClass();
    }
  }

function slideEnd(event) {
    if (sliding == 2) {
      sliding = 0;
      currentSlide = pixelOffset < startPixelOffset ? currentSlide + 1 : currentSlide - 1;
      currentSlide = Math.min(Math.max(currentSlide, 0), slideCount - 1);
      pixelOffset = currentSlide * -$('body').width();
      $('#temp').remove();
      $('<style id="temp">#slides.animate{transform:translate3d(' + pixelOffset + 'px,0,0)}</style>').appendTo('head');
      $('#slides').addClass('animate').css('transform', '');
    }
  }
});

Our Code Explained

Let’s go over these lines of code, shall we?

Within the local scope that jQuery so kindly presents us with $(function() { … });, we start by defining a few required variables, which we’ll explain later. We’ll add cross-platform listeners for basic “dragging” functionality, which we’ll use to do the page sliding:

$('html').live('mousedown touchstart', slideStart);
$('html').live('mouseup touchend', slideEnd);
$('html').live('mousemove touchmove', slide);

The slideStart Method

The slideStart method is triggered on a mousedown or touchstart event; and for cross-platform-ness, we’ll redefine the event parameter to the first touch property in our touches collection (i.e. the first finger) — if we’re on a mobile device, that is:

function slide(event) {
  if (event.originalEvent.touches)
    event = event.originalEvent.touches[0];

Then, we detect whether the user is not sliding yet (sliding == 0) and store the mouse or touch position where the user initiated the slide:

if (sliding == 0) {
  sliding = 1;
  startClientX = event.clientX;
}

The Slide Method

Next, we’ll look at the slide method. This line stores the number of pixels that we’ve moved since our mousedown or touchstart event:

var deltaSlide = event.clientX - startClientX;

So, if sliding was recently set to 1 and deltaSlide is not 0, then that means the user has moved for the first time! We can consider this the actual “slide start” event. Then, we set sliding to 2 (which means the user is actually moving), and we store the current pixels that the entire #slides container was already changed to:

sliding = 2;
startPixelOffset = pixelOffset;

If sliding is set to 2, then that means the user was already moving. So, we set touchPixelRatio to its initial value of 1, which means that the user is sliding exactly 1 pixel for every pixel of mouse or touch movement:

if (sliding == 2) {
  var touchPixelRatio = 1;

So, what does the following code do then?

if ((currentSlide == 0 && event.clientX > startClientX) ||
    (currentSlide == slideCount - 1 && event.clientX < startClientX))
  touchPixelRatio = 3;

Setting touchPixelRatio to 3 means the user has to move their mouse or finger by 3 pixels just to offset the slides by 1 pixel. See what we’re getting at here? Exactly: Apple’s rubber-band effect! The code above checks whether the user is sliding the first slide to the right or the last slide to the left. If they’re doing this, then we set touchPixelRatio to 3.

Then, we calculate the number of pixels that we need to offset #slides.

pixelOffset = startPixelOffset + deltaSlide / touchPixelRatio;

To make the offset in pixels visible, we could use CSS left positioning or a negative left margin, but neither is hardware accelerated. That’s why we’ll use transform:translate3d(x, y, z) to make the offset final:

$('#slides').css('transform', 'translate3d(' + pixelOffset + 'px,0,0)').removeClass();

So, we move x by the pixelOffset’s number of pixels, and we leave y and z alone. The removeClass() will remove the snapping .animation class that we will add in the slideEnd method, which we’ll now dive into.

The slideEnd Method

When the user stops sliding, we want the previous or next slide to smoothly animate into view. First, we need to know which slide should be in view:

function slideEnd(event) {
  if (sliding == 2) {
    sliding = 0;
    currentSlide = pixelOffset < startPixelOffset ? currentSlide + 1 : currentSlide - 1;

Then we min and max it out between 0 and slideCount:

currentSlide = Math.min(Math.max(currentSlide, 0), slideCount - 1);

The value of the final pixelOffset is simple: it’s the viewport’s full width in pixels multiplied by the currentSlide’s number. And because we’re offsetting pixels to the left, we want the negative result:

pixelOffset = currentSlide * -$('body').width();

Almost there. First, take a look at this line of code:

$('<style id="temp">#slides.animate{transform:translate3d(' + pixelOffset + 'px,0,0)}</style>').appendTo('head');

This is the simplest approach to dynamically adding a new CSS rule. We create a style element, with an animate class applied to #slides, which would have changed its translate3d offset to the full slide’s pixeloffset. But because our original style.css file has this line…

.animate {
  -webkit-transition: all .3s ease-out;
  -moz-transition: all .3s ease-out;
  -o-transition: all .3s ease-out;
  -ms-transition: all .3s ease-out;
  transition: all .3s ease-out;
}

… then a CSS animation will kick in using the hardware-accelerated translate3d property. Yay!

To clean up, we remove any existing #temp style elements from the head section prior to creating a new one:

$('#temp').remove();

So, now we’ve got the current (runtime) style set to the current pixelOffset value, and the animate class is set to point #slides to the final position. All we need to do now is swap the style for a class, and the transition will take over:

$('#slides').addClass('animate').css('transform', '');

See? We’ve added the animate class and reset the style property of webkit-transform to nothing.

What Do We Have Now?

If all has gone well, we can open up our work in Chrome, Safari, iPad or iPhone and swipe through these kittens with ease. For those of you who didn’t code along, you can still see the result13.

Remember when we hadn’t yet included our own JavaScript?

Our current result seems very similar to the result as viewed on an iPad or iPhone, doesn’t it? You can swipe to scroll, and it has rubber-banding. But our app snaps in between slides, and the fact that the smoothness of its scrolling is comparable to a native app’s means that hardware acceleration has indeed kicked up the user experience a few notches and brought it closer to the result otherwise seen only in native apps.

And that’s what we set out to do, isn’t it?

As icing on the cake, we can now add features such as a permanent scrub bar at the bottom; round bullets to indicate the number of slides and which slide is active; and a transparent logo to overlay the screen and have the slides move underneath it. I’ve gone ahead and added such features here, so you can view the source14 if you like.

15
View Web app16.

Want Proof That Hardware Acceleration Is Enabled?

If the experience itself doesn’t convince you or your coworker, here’s how to know for sure that the GPU is doing the work. Surf to about:flags in Chrome, enable the FPS counter, and hit the “Relaunch now” button at the bottom of the page.

Chrome offers an FPS counter when hardware acceleration is active.
Chrome offers an FPS counter when hardware acceleration is active.
17

Chrome will now show a red FPS counter in the top-left corner of any screen whenever the GPU is active. If you open our demo now, this is what you’ll see:

Chrome with its FPS counter turned on, which is only visible when the GPU is active
Chrome with its FPS counter turned on, which is visible only when the GPU is active.
18

To see what happens when we do not access the GPU, we need to make some alterations. In style.css, remove this line from the #slides declaration entirely:

transform: translate3d(0,0,0);

In script.js, replace the two occurrences of this…

translate3d(' + pixelOffset + 'px,0,0)

… with this:

translate(' + pixelOffset + 'px,0)

Note that the replacement translate method takes two arguments (x and y) instead of three.

Now, if you refresh the page in Chrome, you’ll see that the FPS counter is not active:

Our Web app with no FPS counter anymore.
Our Web app with no FPS counter anymore.
19

Now that you’ve changed the sliding mechanism from translate3d to translate, try to swipe the kittens again. Notice the jagged behavior?

What Are The Cons?

We have demonstrated that using certain CSS3 properties will kick in the GPU. We have experienced smoother interaction and transitions as a result, so that’s pretty cool. But are there disadvantages to this approach?

Two come to mind — one of which might actually be a pro.

Stability

One thing to keep in mind is that the positive results we’ve seen might not be present in all hardware combinations, because these 3D CSS techniques are relatively new to the Web. On some machines you might experience no hardware acceleration, a garbled screen or no content at all depending on your GPU chip and its support by the browser.

Lucky for us, this does work on all (modern) iOS devices and quite a few newer Android gadgets. I did encounter hiccups on one iMac at the office, but its hardware chip seems to have been whitelisted recently because it seemed to work all of a sudden after the browser (Chrome) updated.

All other PCs and Macs that I tested showed positive results.

Battery Life

To be honest I don’t have statistics on this one. Using the GPU will use battery life, but it saves on CPU cycles, so I guess it could go either way. If you have any information or statistics on this, please share it in the comments below.

Final Thoughts

Knowing when and how to apply hardware acceleration to Web-based transitions and effects can be very helpful when building a mobile Web application or even a game. As the Web evolves, your options grow for creating a friendly experience for end users by speeding up basic things such as the page-sliding transitions that we covered here. And while you can safely use existing Web development frameworks and rely on them to make use of hardware-accelerated CSS when appropriate, having some hands-on experience with tweaking Web applications even further is useful.

(al) (jc)

Footnotes

  1. 1 http://www.sencha.com
  2. 2 http://placekitten.com/
  3. 3 https://developer.apple.com/library/safari/#documentation/appleapplications/reference/SafariHTMLRef/Articles/MetaTags.html
  4. 4 http://www.smashingmagazine.com/wp-content/uploads/2012/02/2-all-slides.png
  5. 5 http://www.smashingmagazine.com/wp-content/uploads/2012/02/3-onlye-1-slide-visible.png
  6. 6 http://www.webkit.org/blog/386/3d-transforms/
  7. 7 http://www.smashingmagazine.com/wp-content/uploads/2012/02/3-iphone.png
  8. 8 http://www.w3schools.com/css3/css3_transitions.asp
  9. 9 http://www.w3.org/TR/selectors/#nth-child-pseudo
  10. 10 http://www.smashingmagazine.com/wp-content/uploads/2012/02/1-basic-scrollable.png
  11. 11 http://jquery.com
  12. 12 http://stackoverflow.com/questions/436411/where-is-the-best-place-to-put-script-tags-in-html-markup
  13. 13 http://webkitten.handcraft.com
  14. 14 http://webkitten2.handcraft.com
  15. 15 http://webkitten2.handcraft.com/
  16. 16 http://webkitten2.handcraft.com/
  17. 17 http://www.smashingmagazine.com/wp-content/uploads/2012/02/about-flags.png
  18. 18 http://www.smashingmagazine.com/wp-content/uploads/2012/02/5-gpu.png
  19. 19 http://www.smashingmagazine.com/wp-content/uploads/2012/02/4-no-gpu.png

↑ Back to topShare on Twitter

Martin Kool is a creative shotgun with sawed-off barrel and partner at Q42. When he's wearing his 13th bulletproof Game Designer suit he comes up with things such as Carrrds, Quento, Numolition or Flippy Bit And The Attack Of The Hexadecimals Of Base 16. He can also be held responsible for the retro adventure game portal Sarien.net and co-founding HTML prototyping service Handcraft. He lives in the Netherlands with his wife and four kids and has been spotted eating pindarotsjes.

Advertising

Note: Our rating-system has caused errors, so it's disabled at the moment. It will be back the moment the problem has been resolved. We're very sorry. Happy Holidays!

  1. 1

    Gunnar Bittersmann

    June 22, 2012 1:42 am

    “We’ll just use the -webkit- vendor prefix…”
    That’s really really sad! You give a bad example. You break the Web.

    Developers: Don’t do this!
    Article writers: Don’t do this!
    Use prefixes for all browsers (at least the major ones) that support that feature (or plan to do so in near future). Use -moz-, -ms-, -o-, and -webkit-.
    End of sermon.

    • 2

      Vitaly Friedman

      June 24, 2012 12:11 am

      You are absolutely right, Gunnar. We have edited the article to make it more clear that it’s developer’s responsibility to use prefixes for all major browsers, not just WebKit.

      • 3

        I think it’s pretty clear when they say: We’ll only use this or that for brevity’s sake.

        We know we have to develop our sites cross browser—in most cases, always depends on audience and situation of course.

        It’s an article to teach us something, not a real life project.

        PS:
        What I wanted to add: Never use CSS shorthand if you don’t need to declare all it’s properties: you often declare/overwrite stuff you don’t intend to.

        In this article
        .slide { background-size: cover; }
        is overwritten by
        .slide:nth-child(){background: url(…)}

        I also use
        margin-right: auto; margin-left: auto;
        when I want to horizontally center stuff, because using
        margin: 0 auto;
        overwritet top and bottom margins to 0

        If you write code for yourself it’s often not a problem, you know what you’re doing, but if someone else has to work on the project later on it can lead to confusing behavior

  2. 4

    Hi Gunnar,

    This article does not attempt to be an example of how to make a cross-browser example for all mobile devices out there, but to illustrate the difference that GPU acceleration can make on mobile devices and – as the title suggests – “play” with that and get your coding hands dirty.

    Not all situations require one to support all browsers out there (a client could target a specific market) and not breaking the web requires more effort than merely including most vendor prefixes, as some vendor/device-combinations will not pass the 3d translation to the GPU and instead emulate it through software rendering, costing performance instead of gaining it.

    I have tried to avoid the cross browser debate by clearly stating the playfulness of the meat of what is covered here up front and why we’re going with a browser vendor prefix that does give the desired results for the end-user to compare.

    I’m sorry if this has caused any confusion.
    Cheers.

    Martin

  3. 5

    Great article, wish this have been published like 5 months ago and I would have avoided a lot of trouble inventing all of this by myself. One note though: to make the code more semantic, elements should have been used instead of divs.

  4. 6

    I wonder why on the first view the images are kind of “built up” of square blocks on the iPad when you swipe to next image? Does it have to do with the GPU memory or something? Once you’ve swiped through them once, they no longer have this annoying behaviour

  5. 7

    Broken in iOS6. :(
    Guess they don’t like smooth animation…

  6. 8

    I enjoyed this article and it was very useful to me.

    Where do I go from here to make the swipe gesture feel more native? e.g. Have vertical scroll but horizontal swipe. Have a minimum swipe distance to trigger the animation, etc.

    I’m having a very hard time finding documentation on native feeling touch gestures in responsive websites, without resorting to frameworks like Sencha.

  7. 10

    Hi,

    Great article! I’ve learned a lot from it. What bugs me from a couple of days though–and I don’t usually ask for help until I know for certain I really can’t figure things out myslef–is that the “event.preventDefault();” statement also blocks the scroll event on touch enabled devices and if my slideshow sits inside a “div” element (not the entire viewport) with content above and below it, users can’t scroll once the screen is occupied by the slideshow.

    My question is: How can I enable the scroll event without affecting the sliding function? (removing the prevent statement results in a buggy sliding experience unfortunately).

    I tried to fake the scroll by getting the current scrolling position and create a deltaY and then increasing that value, but it seems it doesn’t work as I expected.

    Any advice will be much appreciated!

  8. 11

    Thanks for the nice tutorial, i appreciate this as i’m right now beginning to learn HTML5 and CSS3.
    I see one strange behaviour, as i’ve put your code on my webserver (http://netandif.de/html5test.html) i’ve found out that the page works pretty well within Firefox but not so in Chrome. What pretty suprises me then, is that it’s the very opposite on the example’s test page (http://webkitten.handcraft.com/), where it works in Chrome but not in Firefox (both latest versions)

  9. 13

    To make this article more future-proof I would suggest to use the $.fn.on-method for DOM-event-binding, as the $.fn.live-method is deprecated since version 1.7. Also caching jQuery-elements is always a nice thing to do.

    Nevertheless an interesting article!

  10. 14

    you shouldn’t use removeClass() like that… it’ll break if other classes for styling are applied.

  11. 15

    Took me about 4 seconds to break the demo on a desktop (Mini with Chrome). Click-Drag-Releasing outside the window throws the slides positions out of wack. So does long click-dragging it seems…

  12. 16

    Hi,

    Great tutorial, but it doesn’t work…
    script.js seems to be missing (I guess it go delete from wherever it is hosted), so also cannot view the source code.

    Cheers,
    Eugene

  13. 17

    Yup, this is broken:

    Failed to load resource: the server responded with a status of 404 (Not Found) http://webkitten2.handcraft.com/script.js

  14. 18

    Excellent article, thank you.

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top