Ditching jQuery
This article details how I write modern, native JavaScript (aka vanilla JS), and includes a growing collection of native JavaScript equivalents for common jQuery tasks.
Table of Contents
- Why go native?
- My approach
- Native JavaScript APIs
- Selectors
- Looping through objects
- Class manipulation
- Manipulate styles
- Manipulate attributes
- Event listeners
- Waiting until the DOM is ready
- Manipulate height
- Working with forms
- HTML content
- Extend
- Is an element in the viewport?
- Get distances to the top of the document
- Get document height
- Climb up the DOM
- Climb down the DOM
- Get sibling elements
- Get a querystring
- Get HTML from another page
- Get JSON Data
- Working with AJAX and APIs
- Learn more
Why go native?
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.
It’s not just download sizes that you should be worried about. In a presentation given at Velocity in 2011, Maximiliano Firtman pointed out that on some phones (older, but still popular, BlackBerry devices for example) can take up to 8 seconds just to parse jQuery. More recent research from Stoyan Stefanov revealed that even on iOS 5.1, it was taking as many as 200-300ms to parse jQuery.
And while I unfortunately don’t have hard numbers to back it up, I have found that converting the sites I build over to native JavaScript has had a dramatic impact on site performance.
Update: Tim’s got some updated data, and while modern iOS and Android devices are lightning fast, older models—including ones that are still on the market—performed much more poorly.
My approach
The web is for everyone, but support is not the same as optimization.
Rather than trying to provide the same level of functionality for older browsers, I use progressive enhancement to serve a basic experience to all browsers (even Netscape and IE 5). Newer browsers that support modern APIs and techniques get the enhanced experience.
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 feature detection technique that the BBC calls “cutting the mustard.”
A simple browser test determines whether or not a browser supports modern JavaScript APIs. If it does, it gets the enhanced experience. If not, it gets a more basic one.
var supports = !!document.querySelector && !!window.addEventListener;
if ( !supports ) return;
The !!
converts the API method into a boolean value that you can test against. You can (and should) add all of the APIs you need to test against to this list.
The !supports
if statement stops running the script of the browser doesn’t support the appropriate APIs.
What browsers are supported?
To quote the BBC:
- 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)
Hiding content after the JS is loaded
In my scripts, after the mustard test is run, I include this line which adds a class to the <html>
element after the script is loaded.
document.documentElement.className += ' js-MyPlugin';
In my CSS, I can prefix styles with .js-MyPlugin
to ensure that JS-specific styles are only applied in supported browsers after the required files have downloaded. This does result in some FOUT, but it’s worth it to ensure that users can always access content.
Native JavaScript APIs
Below is a growing list of native JavaScript equivalents of jQuery APIs. Unless otherwise noted, these provide support for IE9 and above.
Quick aside: Many modern web and ECMAScript 5 APIs were influenced by jQuery, and have made working on the web much easier. Thanks jQuery!
Selectors
Native JavaScript HTML5 provides two APIs to select elements in the DOM. document.querySelector()
gets the first matching element, while document.querySelectorAll()
returns a node list of all matching elements.
Also supported in IE8, but only for CSS 2.1 selectors.
var firstClass = document.querySelector( '.some-class' );
var firstId = document.querySelector( '#some-id' );
var firstData = document.querySelector( '[data-example]' );
var allClasses = document.querySelectorAll( '.some-class' );
var allData = document.querySelectorAll( '[data-example]' );
Looping through objects
Iterate over arrays, objects, and node lists. Supported all the way back to IE6.
// Arrays and node lists
var elems = document.querySelectorAll( '.some-class' );
for ( var i = 0, len = elems.length; i < len; i++ ) {
console.log(i) // index
console.log(elems[i]) // object
}
// Objects
var obj = {
apple: 'yum',
pie: 3.214,
applePie: true
};
for ( var prop in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) {
console.log( prop ); // key
console.log( obj[prop] ); // value
}
}
Note: Todd Motto has created a simple helper method that’s useful if you frequently loop over objects.
Class manipulation
Add, remove, and check for classes. The classList
API support starts with IE10, but a polyfill provides support back to IE8. You should always use it.
var elem = document.querySelector( '#some-element' );
elem.classList.add( 'some-class' ); // Add class
elem.classList.remove( 'some-other-class' ); // Remove class
elem.classList.toggle( 'some-other-class' ); // Add or remove class
if ( elem.classList.contains( 'some-third-class' ) ) { // Check for class
console.log( 'yep!' );
}
Manipulate styles
Get and set inline styles. This is supported all the way back to IE6.
var elem = document.querySelector( '#some-element' );
var color = elem.style.color; // Get a CSS attribute
elem.style.color = 'rebeccapurple'; // Set a CSS attribute
var height = elem.style.minHeight; // Get a CSS attribute
elem.style.minHeight = '200px'; // Set a CSS attribute
Note: Not sure what the right property name is? I Google these all the time!
Manipulate attributes
Add, remove, and check for attributes.
var elem = document.querySelector( '#some-element' );
elem.getAttribute( 'data-example' ); // Get data attribute
elem.setAttribute( 'data-example', 'Hello world' ); // Set data attribute
if ( elem.hasAttribute( 'data-example' ) ) { // Check data attribute
console.log( 'yep!' );
}
You can use these APIs to get and set all sorts of attributes—not just data attributes. However, there’s usually an API you can call directly on the element, too.
var elem = document.querySelector( '#some-element' );
// Set an ID
elem.setAttribute( 'id', 'new-id' );
elem.id = 'new-id';
// Set width
elem.setAttribute( 'width', '200px' );
elem.width = '200px';
// Get title
var title = elem.getAttribute( 'title' );
var titleToo = elem.title;
Event listeners
Listen for clicks, hovers, and more.
var elem = document.querySelector( '.some-class' );
elem.addEventListener( 'click', function( event ) {
// Do stuff
}, false);
Unlike jQuery, each event requires its own listener, but you can assign a function to a variable to keep your code more DRY.
var elem = document.querySelector( '.some-class' );
var someFunction = function ( event ) {
// Do stuff
};
elem.addEventListener( 'click', someFunction, false );
elem.addEventListener( 'mouseover', someFunction, false );
And if you need to pass multiple variables into a function assigned to a variable, use the .bind()
API. The first variable is the one assigned to this
, and event is automatically passed in as the last variable.
var elem = document.querySelector( '.some-class' );
var someFunction = function ( var1, var2, var3, event ) {
// Do stuff
}
elem.addEventListener('click', someFunction.bind( null, var1, var2, var3 ), false);
elem.addEventListener('mouseover', someFunction.bind( null, var1, var2, var3 ), false);
Note: .bind()
was a late addition to ECMAScript 5, and some otherwise compliant browsers don’t support it. You should include the polyfill if you use it.
With named functions, you can also remove event listeners.
elem.removeEventListener( 'click', someFunction, false );
elem.removeEventListener( 'mouseover', someFunction, false );
If you need to apply the same event listener on multiple elements, you can loop through each element and add a listener. A better and more performant approach, though, is to listen for events on the entire document and filter just the elements you need.
/**
* Function to filter what's clicked and run your functions
* @param {Event} event The event
*/
var eventHandler = function ( event ) {
// Get the clicked element
var toggle = event.target;
// If clicked element is the one you're looking for, run your methods
if ( toggle.hasAttribute( 'data-example' ) || toggle.classList.contains( 'sample-class' ) ) {
event.preventDefault(); // Prevent default click event
someMethod( the, arguments, to, pass, into, method );
}
};
// Listen for all click events on the document
document.addEventListener( 'click', eventHandler, false );
Waiting until the DOM is ready
Run JS methods after the DOM is ready. While modern browsers support the DOMContentReady
event listener, code won’t execute if it’s called after the DOM is loaded (the event it’s listening for has already happened). The ready()
method provided below executes your scripts immediately if the DOM is ready, and waits until it is if it’s not.
Under readyState
, interactive
runs once the document is done but before all images and stylesheets have been downloaded. complete
runs after that stuff is downloaded, too. I’ve included both for completeness.
/**
* Run event after DOM is ready
* @param {Function} fn Callback function
*/
var ready = function ( fn ) {
// Sanity check
if ( typeof fn !== 'function' ) return;
// If document is already loaded, run method
if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
return fn();
}
// Otherwise, wait until document is loaded
document.addEventListener( 'DOMContentLoaded', fn, false );
};
// Example
ready(function() {
// Do stuff...
});
Manipulate height
Get and set height. It’s a lot trickier in native JS than it should be, because there are multiple APIs for getting height, and they all return slightly different measurements. The getHeight()
method provided below returns the largest measurement. These are supported back to IE6.
/**
* Get the height of an element
* @param {Node} elem The element
* @return {Number} The height
*/
var getHeight = function ( elem ) {
return Math.max( elem.scrollHeight, elem.offsetHeight, elem.clientHeight );
};
var elem = document.querySelector( '#some-element' );
var height = getHeight( elem ); // Get height
elem.style.height = '200px'; // Set height
Working with forms
Get field types, input content and states. These are supported back to IE6.
var form = document.querySelector( '#some-form' );
var input = document.querySelector( '#some-input' );
var forms = document.forms; // Get all forms on a page
var formElems = form.elements; // Get all form elements
var inputType = input.type.toLowerCase(); // Get input type and convert to lowercase (radio, checkbox, text, etc.)
var inputValue = input.value; // Get input value
var inputName = input.name; // Get input name
var isChecked = input.checked; // Get the checked status of a checkbox or radio button
var isDisabled = input.disabled; // Get input disabled status
HTML content
Get and set HTML content.
var elem = document.querySelector( '#some-element' );
var html = elem.innerHTML; // Get HTML
elem.innerHTML = 'Hello world!'; // Set HTML
Extend
Merge two or more objects together. The jQuery $.extend()
API merges the content of subsequent objects into the first one, overriding it’s original values. The method provided below returns a new object instead, preserving all of the original objects and their properties. Supported back to IE6.
/**
* Merge two or more objects. Returns a new object.
* Set the first argument to `true` for a deep or recursive merge
* @param {Boolean} deep If true, do a deep (or recursive) merge [optional]
* @param {Object} objects The objects to merge together
* @returns {Object} Merged values of defaults and options
*/
var extend = function () {
// Variables
var extended = {};
var deep = false;
var i = 0;
var length = arguments.length;
// Check if a deep merge
if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
deep = arguments[0];
i++;
}
// Merge the object into the extended object
var merge = function ( obj ) {
for ( var prop in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) {
// If deep merge and property is an object, merge properties
if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) {
extended[prop] = extend( true, extended[prop], obj[prop] );
} else {
extended[prop] = obj[prop];
}
}
}
};
// Loop through each object and conduct a merge
for ( ; i < length; i++ ) {
var obj = arguments[i];
merge(obj);
}
return extended;
};
// Example objects
var object1 = {
apple: 0,
banana: { weight: 52, price: 100 },
cherry: 97
};
var object2 = {
banana: { price: 200 },
durian: 100
};
var object3 = {
apple: 'yum',
pie: 3.214,
applePie: true
}
// Create a new object by combining two or more objects
var newObjectShallow = extend( object1, object2, object3 );
var newObjectDeep = extend( true, object1, object2, object3 );
Is an element in the viewport?
Determine if an element is the viewport or not. Supported back to IE6.
/**
* Determine if an element is in the viewport
* @param {Node} elem The element
* @return {Boolean} Returns true if element is in the viewport
*/
var isInViewport = function ( elem ) {
var distance = elem.getBoundingClientRect();
return (
distance.top >= 0 &&
distance.left >= 0 &&
distance.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
distance.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
var elem = document.querySelector( '#some-element' );
isInViewport( elem ); // Boolean: returns true/false
Get distances to the top of the document
Get your current position from the top of the page, or that of an element.
// Get current location's distance from the top of the page
var position = window.pageYOffset;
/**
* Get an element's distance from the top of the page
* @param {Node} elem The element
* @return {Number} Distance from the top of the page
*/
var getElemDistance = function ( elem ) {
var location = 0;
if ( elem.offsetParent ) {
do {
location += elem.offsetTop;
elem = elem.offsetParent;
} while ( elem );
}
return location >= 0 ? location : 0;
};
var elem = document.querySelector( '#some-element' );
var location = getElemDistance( elem );
Get document height
Get the height of the document element. Supported back to IE6.
/**
* Get the height of the `document` element
* @return {Number} The height
*/
var getDocumentHeight = function () {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight
);
};
Climb up the DOM
Get the parent of an element. Supported back to IE6.
var elem = document.querySelector( '#some-element' );
var parent = elem.parentNode;
Get closest DOM element up the tree that contains any valid CSS selector.
/**
* Get the closest matching element up the DOM tree.
* @private
* @param {Element} elem Starting element
* @param {String} selector Selector to match against
* @return {Boolean|Element} Returns null if not match found
*/
var getClosest = function ( elem, selector ) {
// Element.matches() polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// Get closest match
for ( ; elem && elem !== document; elem = elem.parentNode ) {
if ( elem.matches( selector ) ) return elem;
}
return null;
};
var elem = document.querySelector( '#some-element' );
var closest = getClosest( elem, '.some-class' );
var closestLink = getClosest( elem, 'a' );
var closestExcludingElement = getClosest( elem.parentNode, '.some-class' );
Get all parent elements up the DOM tree, optionally filtering by any valid CSS selector. Includes the element itself.
/**
* Get all of an element's parent elements up the DOM tree
* @param {Node} elem The element
* @param {String} selector A class, ID, data attribute or tag to filter against [optional]
* @return {Array} The parent elements
*/
var getParents = function ( elem, selector ) {
// Element.matches() polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// Setup parents array
var parents = [];
// Get matching parent elements
for ( ; elem && elem !== document; elem = elem.parentNode ) {
// Add matching parents to array
if ( selector ) {
if ( elem.matches( selector ) ) {
parents.push( elem );
}
} else {
parents.push( elem );
}
}
return parents;
};
var elem = document.querySelector( '#some-element' );
var parents = getParents( elem, '.some-class' );
var allParents = getParents( elem.parentNode );
Get all parent elements up the DOM tree until a matching parent is found, optionally filtering by any valid CSS selector. Includes the element itself.
/**
* Get all of an element's parent elements up the DOM tree until a matching parent is found
* @param {Node} elem The element
* @param {String} parent The selector for the parent to stop at
* @param {String} selector The selector to filter against [optionals]
* @return {Array} The parent elements
*/
var getParentsUntil = function ( elem, parent, selector ) {
// Element.matches() polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// Setup parents array
var parents = [];
// Get matching parent elements
for ( ; elem && elem !== document; elem = elem.parentNode ) {
if ( parent ) {
if ( elem.matches( parent ) ) break;
}
if ( selector ) {
if ( elem.matches( selector ) ) {
parents.push( elem );
}
continue;
}
parents.push( elem );
}
return parents;
};
// Examples
var elem = document.querySelector( '#some-element' );
var parentsUntil = getParentsUntil( elem, '.some-class' );
var parentsUntilByFilter = getParentsUntil( elem, '.some-class', '[data-something]' );
var allParentsUntil = getParentsUntil( elem );
var allParentsExcludingElem = getParentsUntil( elem.parentNode );
Climb down the DOM
Get all child nodes of an element. Supported back to IE6.
var elem = document.querySelector( '#some-element' );
var all = elem.childNodes;
Get the first child node of an element. Supported back to IE6.
var elem = document.querySelector( '#some-element' );
var first = elem.firstChild;
Get the first element that matches a class, ID, or data attribute.
var elem = document.querySelector( '#some-element' );
var firstMatch = elem.querySelector( '.sample-class' );
Get all elements that match a class, ID, or data attribute.
var elem = document.querySelector( '#some-element' );
var allMatches = elem.querySelectorAll( '.sample-class' );
Get sibling elements
Get all siblings of an element. Supported back to IE6.
/**
* Get all siblings of an element
* @param {Node} elem The element
* @return {Array} The siblings
*/
var getSiblings = function ( elem ) {
var siblings = [];
var sibling = elem.parentNode.firstChild;
for ( ; sibling; sibling = sibling.nextSibling ) {
if ( sibling.nodeType === 1 && sibling !== elem ) {
siblings.push( sibling );
}
}
return siblings;
};
var elem = document.querySelector( '#some-element' );
var siblings = getSiblings( elem );
Get a querystring
Get a querystring
from a URL. Supported back to at least IE6.
/**
* Get the value of a query string from a URL
* @param {String} field The field to get the value of
* @param {String} url The URL to get the value from [optional]
* @return {String} The value
*/
var getQueryString = function ( field, url ) {
var href = url ? url : window.location.href;
var reg = new RegExp( '[?&]' + field + '=([^&#]*)', 'i' );
var string = reg.exec(href);
return string ? string[1] : null;
};
// http://example.com&this=chicken&that=sandwich
var thisOne = getQueryString( 'this' ); // returns 'chicken'
var thatOne = getQueryString( 'that' ); // returns 'sandwich'
var anotherOne = getQueryString( 'another' ); // returns null
var yetAnotherOne = getQueryString( 'example', 'http://another-example.com&example=something' ); // returns 'something'
Get HTML from another page
Get the contents of another HTML document, or from a specific element in another document. Only works for documents on the same domain. Supported back to IE8 and above.
/**
* Get HTML from another URL
* @param {String} url The URL
* @param {Function} success Callback on success
* @param {Function} error Callback on failure
*/
var getURL = function ( url, success, error ) {
// Feature detection
if ( !window.XMLHttpRequest ) return;
// Create new request
var request = new XMLHttpRequest();
// Setup callbacks
request.onreadystatechange = function () {
// If the request is complete
if ( request.readyState === 4 ) {
// If the request failed
if ( request.status !== 200 ) {
if ( error && typeof error === 'function' ) {
error( request.responseText, request );
}
return;
}
// If the request succeeded
if ( success && typeof success === 'function' ) {
success( request.responseText, request );
}
}
};
// Get the HTML
request.open( 'GET', url );
request.send();
};
// Example 1: Generic Example
getURL(
'/about',
function (data) {
// On success...
var div = document.createElement( 'div' );
},
function (data) {
// On failure...
}
);
// Example 2: Find a specific element in the HTML and inject it into the current page
getURL(
'/about',
function (data) {
// Create a div and inject the HTML into it
var div = document.createElement( 'div' );
div.innerHTML = data;
// Find the element you're looking for in the div
var elem = div.querySelector( '#some-element' );
var location = document.querySelector( '#another-element' );
// Quit if the element or the place where you want to inject it don't exist
if ( !elem || !location ) return;
// Inject the element into the DOM
location.innerHTML = elem.innerHTML;
}
);
Get JSON Data
Get JSON data from another server. Supported back to IE6.
/**
* Get JSONP data
* @param {String} url The JSON URL
* @param {Function} callback The function to run after JSONP data loaded
*/
var getJSONP = function ( url, callback ) {
// Create script with url and callback (if specified)
var ref = window.document.getElementsByTagName( 'script' )[ 0 ];
var script = window.document.createElement( 'script' );
script.src = url + (url.indexOf( '?' ) + 1 ? '&' : '?') + 'callback=' + callback;
// Insert script tag into the DOM (append to <head>)
ref.parentNode.insertBefore( script, ref );
// After the script is loaded (and executed), remove it
script.onload = function () {
this.remove();
};
};
// Example
var logAPI = function ( data ) {
console.log( data );
}
getJSONP( 'http://api.petfinder.com/shelter.getPets?format=json&key=12345&shelter=AA11', 'logAPI' );
Working with AJAX and APIs
For robust API interactions, check out Atomic by Todd Motto, supported back to IE8.
Learn More
My first go-to site for anything JavaScript related is the Mozilla Developer Network, which is essentially a user guide for the web. They provide documentation on tons of web and JS APIs, with examples, browser compatibility information, and polyfills when needed. Just add mdn
to your Google searches.
If that fails, I turn to Stack Overflow. Make sure to add vanilla js
to your searches. Typing without jQuery
returns a ton of jQuery-based responses instead.