Skip to main content Skip to secondary navigation Accessibility Feedback

Ditching jQuery for Vanilla JS

Note: I’ve written a new version of this article that reflects how I work with native JavaScript today.

On the recent launch of a new site for professional photographer Tony Luong, we dropped jQuery in favor of 100% vanilla JavaScript.

It had a dramatic impact on website performance. Today, I want to share the process for going framework-free.

Psst... I'm writing a book on ditching jQuery for vanilla JS. Pre-order it today and save 50%.

Different browsers. Different features. #

One of the benefits of a framework like jQuery is that it smooths out all of the weird browser inconsistencies you might run into. But, all that abstraction and extra code adds a lot of weight and performance latency to a site.

Rather than trying to provide the same level of functionality for older browsers, I took a progressive enhancement approach to development. Older and less capable browsers get a basic experience. Newer browsers that support modern APIs get an enhanced one.

To be clear, I’m not advocating dropping support for older and less capable browsers. They still have access to all of the content. They just don’t always get the same layout or extra features.

Cutting the Mustard #

I used a simple feature detection technique coined by the BBC as “cutting the mustard.” This one “if statement” tests support for modern JavaScript APIs:

if ( 'querySelector' in document && 'addEventListener' in window ) {
    // Scripts go here
}

The BBC explains why these were chosen:

document.querySelector – A large part of any JS library is its DOM selector. If the browser has native CSS selecting then it removes the need for a DOM selector. QuerySelector has been available in Firefox since 3.5 at least and has been working in webkit for ages. It also works in IE9.

window.addEventListener – Another large part of any JS library is event support. Every browser made in the last 6 years (except IE8) supports DOM level 2 events. If the browser supports this then we know it has better standards support than IE8.

The BBC also tests for local storage support, but since none of my scripts use that at this time, I left it out. I did, however, add a check for forEach loops, which are a newer API that I use often.

Note: This section was updated on August 16, 2013 to include forEach loops as part of the “cut the mustard” test, and again on August 20, 2014 to include a better approach.

So what browsers does that support? #

The “cut the mustard” test splits browsers into two categories: HTML5 browsers, and not-HTML5 browsers. From the BBC, here’s how common browsers get split up:

HTML5 browsers:

  • IE9+
  • Firefox 3.5+
  • Opera 9+ (and probably further back)
  • Safari 4+
  • Chrome 1+ (I think)
  • iPhone and iPad iOS1+
  • Android phone and tablets 2.1+
  • Blackberry OS6+
  • Windows 7.5+ (new Mango version)
  • Mobile Firefox (all the versions we tested)
  • Opera Mobile (all the versions we tested)

HTML4 browsers:

  • IE8
  • Blackberry OS5
  • Nokia S60 v6
  • Nokia S40 (all versions)
  • All other Symbian variants
  • Windows 7 phone (pre-Mango)
  • …and many more that are too numerous to mention

Bootstrapping in your code #

Up in the document <head>, I include this little snippet of code:

if ( 'querySelector' in document && 'addEventListener' in window ) {
    document.documentElement.className += 'js';
}

This does the mustard test, and if the browser passes, adds a .js class to the <html> element.

If any of my scripts also rely on CSS, I make them conditional on the presence of that class. That way, nothing is ever hidden or inaccessible for someone whose browser doesn’t pass the test.

By putting that script in the head, before the content has even loaded, you ensure that users with modern browsers don’t see any weird style changes after the page loads.

In my JavaScript file (yes, just one), I use the same mustard test as a wrapper for my scripts:

if ( 'querySelector' in document && 'addEventListener' in window ) {
    // Scripts go here
}

Working with selectors #

Modern JavaScript APIs make working with selectors a lot easier.

For getting objects by ID, I use querySelector. For example, if I wanted to get a div with the ID of #turkey, I’d do this:

var sandwichTurkey = document.querySelector('#turkey');

If you’ve never used querySelector before, it returns the first element on a page that matches the selector. For IDs, there should only be one match per page. For classes, it would return the first element with that class.

To get all objects with a particular class, I use querySelectorAll. For example, to get all elements with the .sandwich class, I’d do this:

var sandwiches = document.querySelectorAll('.sandwich');

It returns an array of elements a node list that I can loop through and work with.

Looping through elements #

If you get all elements with a particular class, you most likely want to do something with them. Doing so is easy with a for loop:

var sandwiches = document.querySelectorAll('.sandwich');

for (var i = 0, len = sandwiches.length; i < len; i++) {
    console.log(i) // index
    console.log(sandwiches[i]) // element
}

Event listeners #

Taking action when someone clicks, uses their keyboard, and more is pretty easy thanks to the addEventListener method.

If I wanted to do something whenever anyone clicked a link with the #turkey ID, I'd use this script:

var sandwichTurkey = querySelector('#turkey');

sandwichTurkey.addEventListener('click', function(e) {
    // Do stuff
}, false);

Working with classes #

One of awesome new JavaScript methods is classList, that let's you easily add, remove and toggle classes just like you would in jQuery. Unfortunately, it still lacks the browser support it needs for every day use.

Update on June 9, 2014: I'm now including Eli Grey's classList.js polyfill in my projects. It extends support back to IE 8, and when classList support is good enough, I can just remove the polyfill from my projects without updating any of my code.

In the interim, Todd Motto has created some awesome class handler functions that I include in every project:

var hasClass = function (elem, className) {
    return new RegExp(' ' + className + ' ').test(' ' + elem.className + ' ');
}

var addClass = function (elem, className) {
    if (!hasClass(elem, className)) {
        elem.className += ' ' + className;
    }
}

var removeClass = function (elem, className) {
    var newClass = ' ' + elem.className.replace( /[\t\r\n]/g, ' ') + ' ';
    if (hasClass(elem, className)) {
        while (newClass.indexOf(' ' + className + ' ') >= 0 ) {
            newClass = newClass.replace(' ' + className + ' ', ' ');
        }
        elem.className = newClass.replace(/^\s+|\s+$/g, '');
    }
}

These three simple functions let you test for the presence of a class, add a class, and remove a class. In conjunction, they make basic DOM manipulation really easy.

Update on August 27, 2013: I've combined these class handlers, along with a few other useful functions, into a vanilla JS micro-library called Buoy that I now use on all of my projects.

Update on April 20, 2014: Buoy no longer uses these class handlers, and has been converted to a classList polyfill forked from classList.js by Eli Grey.

Update on June 9, 2014: Since I'm using Eli Grey's classList.js polyfill, Buoy is no longer needed and has been removed from GitHub. If you'd prefer to use class handlers, though, check out Apollo from Todd Motto.

Putting it all together #

Let's say I have a set of links with the .sandwich class. Whenever one of them is clicked, I want to see if it has the .mustard class well. If it does, I'd like to remove it, and if it doesn't, I want to add it.

Here's what that would look like:

// Class Handlers
var hasClass = function (elem, className) {
    return new RegExp(' ' + className + ' ').test(' ' + elem.className + ' ');
}

var addClass = function (elem, className) {
    if (!hasClass(elem, className)) {
        elem.className += ' ' + className;
    }
}

var removeClass = function (elem, className) {
    var newClass = ' ' + elem.className.replace( /[\t\r\n]/g, ' ') + ' ';
    if (hasClass(elem, className)) {
        while (newClass.indexOf(' ' + className + ' ') >= 0 ) {
            newClass = newClass.replace(' ' + className + ' ', ' ');
        }
        elem.className = newClass.replace(/^\s+|\s+$/g, '');
    }
}

// Sandwich Functions
if ( 'querySelector' in document && 'addEventListener' in window ) {

    var sandwiches = document.querySelectorAll('.sandwich');

    for (var i = 0, len = sandwiches.length; i < len; i++) {
        var sandwich = sandwiches[i];
        sandwich.addEventListener('click', function(e) {

            if ( hasClass(sandwich, 'mustard') ) {
                removeClass(sandwich, 'mustard');
            }

            else {
                addClass(sandwich, 'mustard');
            }

        }
    }

}

Less jQuery. More vanilla JS. #

My experience implementing a 100% vanilla JS experience for Tony's new site was a lot of fun. Modern JavaScript API's are powerful and easy to work with, and the performance results are extraordinary.

I'm sure I'll still use jQuery for projects that require more backwards compatibility or involve more complexity, but I'll be using it a lot less, and vanilla JS a lot more.

  1. Tony Luong's new site
  2. BBC on "cutting the mustard"
  3. Todd Motto on moving from jQuery to vanilla JS
  4. Todd Motto on working with classes in vanilla JS
  5. Accessible JavaScript
  6. High-performance websites
  7. Function statements vs function expressions (or, why functions are variables)
  8. Todd Motto's forEach method
If you liked this article, you might also like Ditching jQuery, my new beginner's guide to vanilla JavaScript. Pre-order it today and save 50%.

Have any questions or comments about this post? Email me at chris@gomakethings.com or contact me on Twitter at @ChrisFerdinandi.

Get Weekly Digests