HTML5, CSS3, and the Bookmarklet that Shook the Web

On Valentine’s Day last year we released a bookmarklet that went viral riding the popularity of the Harlem Shake meme. On the anniversary of its release we’d like to take a moment look back at the technical nuts and bolts of the bookmarklet as a case study in applying HTML5. In fact, the HTML, JavaScript, and CSS we used wouldn’t have worked on a single browser a few years ago. What follows is a technical discussion on how we took advantage of recent browser developments to shake up the web.

Background

Last year the Harlem Shake meme forced itself on to nearly every screen under the sun and, like everyone else, we had joked about doing our office version of the video. After tossing around a few bad video ideas, Ishan half-jokingly suggested a bookmarklet that made a web page do the Harlem Shake. Omar and Hari immediately jumped on the ingenuity of his idea and built a prototype within an hour that had the entire office LOLing. After pulling a classic all nighter we released it on February 14th, declaring “Happy Valentine’s Day, Internet! Behold, the Harlem Shake Bookmarklet”.

Pretty soon it was picked up by news outlets like TechCrunch and HuffingtonPost, and our traffic skyrocketed. Meanwhile the bookmarklet offered a new avenue of expression in the watch-then-remix cycle that is the lifeblood of a viral meme like the Harlem Shake. Instead of creating a video of people dancing, developers could now remix this symbiotic meme in code. Startups like PivotDesk incorporated the bookmarklet into their websites, and HSMaker used the code to build a Harlem-Shake-As-A-Service website. Eventually, YouTube even built their own version as an easter egg on their site.

So, how does it work?

Once you click the Harlem Shake bookmark, a snippet of JS is evaluated on the webpage, just as you’d see by entering javascript:alert(“Hi MozHacks!”); in your address bar. This JavaScript will play the Harlem Shake audio, “shake” DOM nodes (according to timing events attached to the audio), and remove all DOM changes afterward.

How did we attach the audio to the page and get the timing for the shakes just right?

HTML5’s extensive audio support made this implementation fairly easy. All that was required was inserting an <audio> tag with the src pointed to the Harlem_Shake.ogg file. Once inserted into the DOM, the file would begin downloading, and playback begins once enough of the file has been buffered.

HTML5 timed audio events allow us to know exactly when playback begins, updates, and ends. We attach a listener to the audio node which evaluates some JS once the audio reaches certain time. The first node starts shaking once the song is beyond 0.5s. Then, at 15.5s, we flash the screen and begin shaking all of the nodes. At 28.5s, we slow down the animations, and once the audio has ended, we stop all animations and clean up the DOM.

audioTag.addEventListener("timeupdate", function() {
  var time = audioTag.currentTime,
      nodes = allShakeableNodes,
      len = nodes.length, i;

  // song started, start shaking first item
  if(time >= 0.5 && !harlem) {
    harlem = true;
    shakeFirst(firstNode);
  }

  // everyone else joins the party
  if(time >= 15.5 && !shake) {
    shake = true;
    stopShakeAll();
    flashScreen();
    for (i = 0; i < len; i++) {
      shakeOther(nodes[i]);
    }
  }

  // slow motion at the end
  if(audioTag.currentTime >= 28.4 && !slowmo) {
    slowmo = true;
    shakeSlowAll();
  }
}, true);

audioTag.addEventListener("ended", function() {
  stopShakeAll();
  removeAddedFiles();
}, true);

How did we choose which parts of the page to shake?

We wrote a few helpers to calculate the rendered size of a given node, determine whether the node is visible on the page, and whether its size is within some (rather arbitrary) bounds:

var MIN_HEIGHT = 30; // pixels
var MIN_WIDTH = 30;
var MAX_HEIGHT = 350;
var MAX_WIDTH = 350;

function size(node) {
  return {
    height: node.offsetHeight,
    width: node.offsetWidth
  };
}
function withinBounds(node) {
  var nodeFrame = size(node);
  return (nodeFrame.height > MIN_HEIGHT &&
          nodeFrame.height < MAX_HEIGHT &&
          nodeFrame.width > MIN_WIDTH &&
          nodeFrame.width < MAX_WIDTH);
}
// only calculate the viewport height and scroll position once
var viewport = viewPortHeight();
var scrollPosition = scrollY();
function isVisible(node) {
  var y = posY(node);
  return (y >= scrollPosition && y <= (viewport + scrollPosition));
}

We got a lot of questions about how the bookmarklet was uncannily good at iniating the shake on logos and salient parts of the page. It turns out this was the luck of using very simple heuristics. All nodes are collected (via document.getElementsByTagName(“*”)) and we loop over them twice:

  1. On the first iteration, we stop once we find a single node that is within the bounds and visible on the page. We then start playing the audio with just this node shaking. Since elements are searched in the order they appear in the DOM (~ the order on the page), the logo is selected with surprising consistency.
  2. After inserting the audio, we have ~15 seconds to loop through all nodes to identify all shakeable nodes. These nodes get stored in an array, so that once the time comes, we can shake them.
// get first shakeable node
var allNodes = document.getElementsByTagName("*"), len = allNodes.length, i, thisNode;
var firstNode = null;
for (i = 0; i < len; i++) {
  thisNode = allNodes[i];
  if (withinBounds(thisNode)) {
    if(isVisible(thisNode)) {
      firstNode = thisNode;
      break;
    }
  }
}

if (thisNode === null) {
  console.warn("Could not find a node of the right size. Please try a different page.");
  return;
}

addCSS();

playSong();

var allShakeableNodes = [];

// get all shakeable nodes
for (i = 0; i < len; i++) {
  thisNode = allNodes[i];
  if (withinBounds(thisNode)) {
    allShakeableNodes.push(thisNode);
  }
}

How did we make the shake animations not lame?

We utilized and tweaked Animate.css’s library to speed up the process, its light and easy to use with great results.

First, all selected nodes gets a base class ‘harlem_shake_me’ that defines animation parameters for duration and how it should apply the styles.

.mw-harlem_shake_me {
  -webkit-animation-duration: .4s;
     -moz-animation-duration: .4s;
       -o-animation-duration: .4s;
          animation-duration: .4s;
  -webkit-animation-fill-mode: both;
     -moz-animation-fill-mode: both;
       -o-animation-fill-mode: both;
          animation-fill-mode: both;
}

The second set of classes that defines the animation’s behavior are randomly picked and assigned to various nodes.

@-webkit-keyframes swing {
  20%, 40%, 60%, 80%, 100% { -webkit-transform-origin: top center; }
  20% { -webkit-transform: rotate(15deg); }
  40% { -webkit-transform: rotate(-10deg); }
  60% { -webkit-transform: rotate(5deg); }
  80% { -webkit-transform: rotate(-5deg); }
  100% { -webkit-transform: rotate(0deg); }
}

@-moz-keyframes swing {
  20% { -moz-transform: rotate(15deg); }
  40% { -moz-transform: rotate(-10deg); }
  60% { -moz-transform: rotate(5deg); }
  80% { -moz-transform: rotate(-5deg); }
  100% { -moz-transform: rotate(0deg); }
}

@-o-keyframes swing {
  20% { -o-transform: rotate(15deg); }
  40% { -o-transform: rotate(-10deg); }
  60% { -o-transform: rotate(5deg); }
  80% { -o-transform: rotate(-5deg); }
  100% { -o-transform: rotate(0deg); }
}

@keyframes swing {
  20% { transform: rotate(15deg); }
  40% { transform: rotate(-10deg); }
  60% { transform: rotate(5deg); }
  80% { transform: rotate(-5deg); }
  100% { transform: rotate(0deg); }
}

.swing, .im_drunk {
  -webkit-transform-origin: top center;
  -moz-transform-origin: top center;
  -o-transform-origin: top center;
  transform-origin: top center;
  -webkit-animation-name: swing;
  -moz-animation-name: swing;
  -o-animation-name: swing;
  animation-name: swing;
}

Shake it like a polaroid picture

What started a joke ended up turning into its own mini-phenomenon. The world has moved on from the Harlem Shake meme but the bookmarklet is still inspiring developers to get creative with HTML5.

If you want to see the full source code or have suggestions, feel free to contribute to the Github repo!

About Hari Ananth

Hari is a Software Engineer at Moovweb working on the Moovweb SDK and mobile applications. Hari has a degree in EECS from UC Berkeley and is outrageously tall.

More articles by Hari Ananth…

About Omar Jalalzada

Omar Jalalzada is a designer of digital products based in San Francisco. Omar is passionate about human behaviors and works with brands to elevate the human attributes of their product and to make them more accessible to their consumers.

More articles by Omar Jalalzada…

About Ishan Anand

Ishan Anand is Director of New Products at Moovweb. He has been launching mobile products for the iPhone since the day it was released, and his work has been featured on TechCrunch, ReadWriteWeb and LifeHacker. Ishan holds dual-degrees in electrical engineering and mathematics from MIT.

More articles by Ishan Anand…

About Robert Nyman [Editor emeritus]

Technical Evangelist & Editor of Mozilla Hacks. Gives talks & blogs about HTML5, JavaScript & the Open Web. Robert is a strong believer in HTML5 and the Open Web and has been working since 1999 with Front End development for the web - in Sweden and in New York City. He regularly also blogs at http://robertnyman.com and loves to travel and meet people.

More articles by Robert Nyman [Editor emeritus]…


11 comments

  1. Caspy7

    I apparently missed this bookmarklet the first time around so I went over to give it a try and it crashed (irreparably froze) Aurora two times in a row – when the second stanza of the music was to begin.
    I tried it in Chrome and the first object shook, but then the tab/page became unresponsive though the animation continued.
    :-/

    February 15th, 2014 at 15:58

    1. Erwan d’Orgeville

      Exactly the same problem.

      February 16th, 2014 at 14:19

    2. voracity

      Crashed my main Firefox, and then crashed my Nightly. How did this not get noticed before posting the demo?

      February 17th, 2014 at 21:32

      1. Robert Nyman [Editor]

        I’ve tried it in main Firefox and Google Chrome, and it has been working for me. Naturally, your mileage may vary, so if the problems persist I recommend opening an issue about them.

        February 18th, 2014 at 02:36

  2. Joshua Smith

    Crashes for me too :-( I tested in FF, Chrome, Safari, and Opera on an Intel computer with OS X 10.9 Mavericks.

    February 18th, 2014 at 13:02

  3. Rodolphe

    Freezes both chrome and firefox here (Ubuntu 12)

    February 20th, 2014 at 03:57

  4. Grant Kemp

    I don’t care what anyone says. That Harlem shake bookmarklet- provides me hours of enjoyment- especially when I warn non techie friends about the harlem shake virus going around.

    Classic- and awesome tech.

    Nice shout in sharing how its done. Rick Roll version next….

    February 20th, 2014 at 14:49

  5. Ishan Anand

    Hi folks, I’m one of the authors of the article. Sorry if you’re having any issues! It certainly was working for us here. As Robert mentioned please log any issue in the github repo https://github.com/moovweb/harlem_shaker/issues and we’ll take a look.

    February 22nd, 2014 at 10:34

  6. Ray

    Crashed my IE,Chrome,Firefox and Opera. That is actually damn impressive.

    March 3rd, 2014 at 17:12

  7. Alex Jones

    Awesome, using it on Chrome book, it has chrome on Steroids.
    20% { transform: rotate(5deg); }
    40% { transform: rotate(-3deg); }
    60% { transform: rotate(2deg); }
    80% { transform: rotate(-2deg); }
    100% { transform: rotate(4deg); } . To create gentle shake .
    Tried implementing some apps here too
    http://www.tutorialspark.com/css3Reference/CSS3_animation_Property.php

    March 4th, 2014 at 13:27

  8. Arun EB

    Awesome but in the key frame animation within the percentage intervals, if i going to use background-image property, it will not working in Firefox.

    Ex.
    @-moz-keyframes rotate-banner{
    0% {
    background:url(../images/banner/HTML5_Wallpaper_1680x1050_03.jpg) 0 0;

    }
    20% {
    background:url(../images/banner/css3_03.jpg) 0 0;

    }
    40% {
    background:url(../images/banner/on-page-seo-banner_03.jpg) 0 0;

    }
    60% {
    background:url(../images/banner/responsive-banner_03.jpg) 0 0;

    }
    80% {
    background:url(../images/banner/banner-bg_03.jpg) 0 0;

    }
    100% {
    background:url(../images/banner/banner_color_03.jpg) 0 0;

    }

    }

    If any solution anything else available to fix this issue in Firefox, please place your comments for us to make using better Firefox to Comparability with CSS3 fully updated.

    March 12th, 2014 at 08:25

Comments are closed for this article.