Opinionated jQuery Style Guide for teams by De Voorhoede.
This guide aims to improve the way your team uses jQuery. It helps you write code which is
- close to modern web technologies and best practices.
- easy to understand for developers / team members.
- easy to reuse (in other projects).
- more performant.
- avoids overuse of jQuery and needless vendor locking.
- About jQuery
- Consider native browser features
- Consider lightweight alternative
- Avoid
.ready()
- Assign
jQuery
to$
- Cache jQuery lookups
- Optimise selectors for performance
- Use
.first()
for single element - Use
.on()
for event binding - Use event delegation
- Avoid
.show()
,.hide()
and.toggle()
- Avoid using
.css()
- Prefer CSS animations over
.animate()
- Prefer native array methods
- Prefer promises over callbacks
- Lint your script files
- Create a smaller build
jQuery is a utility library for easy DOM access & manipulation, event handling, Ajax and more. By using jQuery you can write consise and expressive code which works across modern and legacy browsers. jQuery has extensive tests, detailed documentation, a large active community and an ecosystem of plugins.
Since the release of jQuery many of its features now have a native browser equivalent. For example adding a class to an element can now be achieved via element.classList.add(className)
instead of $(element).addClass(className)
. Depending on the needs of your project you may be able to use only native browser features.
- Native browser features are following the spec, making them future proof.
- Native browser features are closer to the metal, making them faster than jQuery.
- Developers who already know JavaScript don't need to learn the specifics of jQuery.
- Only apply Javascript after feature detection. Use a server-side rendered page as a basis. Enhance parts of a page only if the browser natively supports the required technology.
- Consult platform.html5.org for overview of native browser technologies and Can I use and Kangax tables for browser compatibility.
- Consult you might not need jQuery for native alternatives to jQuery features.
jQuery is the swiss army knife for DOM and event handling and much more. While jQuery offers a wide range of features, you might not need most of them in your project. Simply because you have little functionality or only modern browsers to support. In that case consider a lightweight alternative.
- Lightweight alternatives have a smaller file size and can therefore be downloaded faster.
- Lightweight alternatives have less wrapper functionality which typically makes them faster.
- Some lightweight alternatives support only newer browsers and can therefore stay closer to native browser methods. This typically makes them more performant and easier to understand.
- Some lightweight alternatives mimic the jQuery API. Which means if you know jQuery, you know the alternative.
- Use a lightweight alternative like Cash, Dominus, Shoestring or Zepto.
- Create a jQuery custom build to only include the features you need.
jQuery's .ready()
ensures your script is not executed before the DOM is ready. This is important because we typically want to access the DOM in our script. However, since the script can't be executed before the DOM is ready, a better practice is to defer script execution.
- Using
.ready()
means scripts have been loaded too early. Therefore defer loading instead. - Script loading blocks page rendering. Therefore script loading should be defered.
Avoid loading scripts too early and waiting for the DOM to be ready using .ready()
.
Defer script loading by placing scripts just before the closing </body>
tag or using the defer
attribute:
<!-- recommended: load in tail -->
<body>
...
<script src="path/to/jquery.min.js"></script>
<script>/* DOM is ready, use jQuery directly */</script>
</body>
<!-- recommended: defer script loading -->
<head>
...
<script defer src="path/to/jquery.min.js"></script>
<script defer src="path/to/my-app.min.js"></script>
</head>
Note: Be aware using defer
in IE <= 9 can cause issues. So consider your browser scope before using this setup.
<!-- avoid: -->
<head>
...
<script src="path/to/jquery.min.js"></script>
<script>jQuery.ready(function(){ /* ... */ });</script>
<!-- same applies to external script containing `.ready()` -->
</head>
- Assigning
jQuery
to$
is a common practice, which developers are familiair to. - Assigning
jQuery
to$
within a scope, avoids unexpected conflict with other scripts using$
. - The jQuery documentation and other resources typically use
$
.
Explicitly assign jQuery
to $
within a scope. When using a module loader (like CommonJS) assign it directly to a variable named $
. Otherwise use an IIFE (immediately-invoked function expression):
/* recommended when using module loader, like CommonJS: */
const $ = require('jquery');
// use jQuery as $
/* recommended: use an IIFE */
(function($){
// use jQuery as $
}(jQuery));
Every call to $(element}
asks jQuery to rescan for the matching element, wrap it in a jQuery object, and create a new instance of something you already have in memory. This is something avoidable if you already did it once.
- It avoids querying the DOM for the element everytime want to use it, which greatly improves performance.
- You can use descriptive variable names which convey more meaning.
/* avoid: repeating jQuery lookups */
$('button').addClass('is-active');
$('button').on('click', function(event) {});
/* recommended: cache jQuery lookup in variable */
var $button = $('button');
$button.addClass('is-active');
$button.on('click', function(event) {});
- Always try to create a selector that is exactly specific enough for your case. Make sure you don't depend on a specific HTML structure.
- Creating good selectors makes your HTML more flexible.
- Querying on a specific element is faster than the whole document. This is really easy with module based development.
/* avoid: overly specific */
var $amount = $('[data-table] [data-table-amount]');
var $percentage = $('[data-table] [data-table-percentage]');
/* recommended: using `.find()` which is highly optimised on the parent element */
var $table = $('[data-table');
var $amount = $table.find('[data-table-amount]');
var $percentage = $table.find('[data-table-percentage]');
jQuery always returns a collection when using $(selector)
, while sometimes you are only interested in / only expect one element. In vanilla JS you would use .querySelector(selector)
instead of .querySelectorAll(selector)
.
To make it clear for other developers of you intention of just using one element
/* collection of buttons (akin querySelectorAll) */
$buttons = $form.find('button');
/* versus just a single button (akin querySelector) */
$submitButton = $form.find('[type="submit"]').first();
Naturally variable names should also reflect this. So plural for collections ($buttons
), singular for a individual element ($button
).
Use .on() for event binding
Methods like .click()
or .change()
are just alias for .on('click')
and .on('change')
.
.on()
supports event delegation resulting in more flexibility and better performance.- It's a way to keep consistency as all your events have the same signature.
- Avoiding using aliases let you trim the jQuery custom build. That way you reduce load/parse times and the file size.
/* avoid: .click() */
$button.click(function(event) {});
/* recommended: .on() */
$button.on('click', function() {});
When a selector is provided, the event handler is referred to as delegated. jQuery bubbles the event from the event target up to the element where the handler is attached and runs the handler for any elements along that path matching the selector.
— jQuery
- Using delegated events allows for events to be processed even to elements added to the document later
- Keeps the scope of the event bubbling shorter and thus performant.
$list = $('[todo-list]').first();
$items = $list.find('[todo-item]');
/* avoid: event listener on each item */
$items.on('click', function(event) { /* ... */ });
/* recommended: event delegation on list */
$list.on('click', '[todo-item]', function(event) { /* ... */ });
JQuery lets you .show()
, .hide()
and .toggle()
elements. jQuery toggles an inline display: none
to achieve this.
HTML5 introduces a new global attribute named [hidden]
, which is styled as display: none
by default. It's better practice to use this native standard and toggle [hidden]
instead of using .show()
, .hide()
and .toggle()
.
- Avoid
.show()
,.hide()
and.toggle()
as they use inline styles, which are hard to overwrite. - Prefer
[hidden]
as it semantically indicates an element is not yet, or no longer, relevant. - Styles should be managed in (external) CSS stylesheets (not inlined) for separation of concerns.
Add, remove or toggle the [hidden]
attribute instead of using .show()
, .hide()
or .toggle()
.
Note: If you need to support pre HTML5 browsers use CSS to style [hidden]
correctly:
/* recommended: ensure hidden elements are not displayed in pre HTML5 browsers */
[hidden] { display: none !important; }
/* avoid: `.show()` elements */
$elements.show();
/* recommended: add `[hidden]` attribute */
$elements.attr('hidden', '');
/* avoid: `.hide()` elements */
$elements.hide();
/* recommended: remove `[hidden]` attribute */
$elements.removeAttr('hidden');
/* avoid: `toggle()` elements */
$elements.toggle();
/* recommended: toggle hidden attribute (with jQuery): */
// add `toggleHidden` functionality as jQuery plugin
$.fn.toggleHidden = function() {
return this.each(function(index, element) {
var $element = $(element);
if ($element.attr('hidden')) {
$element.removeAttr('hidden')
} else {
$element.attr('hidden', '');
}
});
};
// call `toggleHidden` on element:
$elements.toggleHidden();
/* recommended: toggle hidden attribute (without jQuery): */
$elements.get().forEach(toggleHidden);
function toggleHidden(element) {
if (element.hasAttribute('hidden')) {
element.removeAttribute('hidden')
} else {
element.setAttribute('hidden', '');
}
}
jQuery can get and set styling directly on an element with the .css()
method.
When using .css()
to set CSS it will set the styles inline and you will be mixing concerns (styling and logic).
- By toggling classes the same thing can be accomplished.
- Separation of concerns, don't mix CSS with JavaScript.
- When the
.css()
method is not used it can be omitted when creating a custom jQuery build. This reduces file size.
/* avoid: mixing JavaScript and CSS */
$element.css('border', '1px solid green');
/* recommended: using a class */
$element.addClass('is-active');
/* CSS */
.is-active { border: 1px solid green; }
jQuery lets you create complex animation sequences using .animate(). Since the introduction of jQuery native CSS has caught up and now also provides methods to transition and animate elements.
- Defining animations in CSS separates presentation from (interaction) logic.
- Native CSS transitions and animations can be hardware-accelerated (using GPU), resulting in smoother animations.
For simple animations use a CSS transition:
/* avoid: jquery animate */
$element.animate({ left: '50px' }, 150, 'easeout');
/* recommended: css animations */
$element.addClass('is-active');
/* vendor prefix might be required */
.is-active {
transform: translate(50px);
transition: transform 150ms ease-out;
}
For more complex animations use a CSS keyframes animation:
/* avoid: jquery animate */
function blink() {
$element
.animate({ opacity: 0 }, 1000)
.animate({ opacity: 1 }, 1000, blink);
}
blink();
/* recommended: css animations */
$element.addClass('is-blinking');
/* vendor prefix might be required */
.is-blinking {
animation: blink 2s infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
jQuery’s array methods are non-standard. They use the signature (index, item/element)
while native uses (item/element, index)
.
Make sure you check your browser scope supports native array methods. Then use .get()
to get a native array of HTML elements. Use native array methods, like forEach
, map
, filter
and reduce
to process the elements:
/* avoid: jQuery array methods */
$elements.map((index, el) => /* ... */)
/* recommended: use native methods */
$elements.get().map(el => /* ... */)
A Promise represents a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers to an asynchronous action's eventual success value or failure reason.
Use promises instead of callbacks to keep code more readable and future proof when handling asynchronous requests. Promises can also be passed around so other modules can chain (.then) onto it, instead of complex callbacks inside callbacks (pyramid of doom) structures.
/* avoid: callbacks */
$.ajax('example.com/articles/1/author', {
success: function(response) {},
error: function(err) {}
});
/* recommended: use promise */
var request = $.ajax('example.com/articles/1/author');
request.then(function(response) {});
request.catch(function(err) {});
Linters like ESLint and JSHint improve code consistency and help trace syntax errors.
- Linting files ensures all developers use the same code style.
- Linting files helps you trace syntax errors before it's too late.
Configure your linter to accept jQuery and $
as global variable.
{
"env": {
"browser": true
},
"globals": {
"jQuery": true,
"$": true
}
}
{
"jquery": true,
"browser": true
}
Special builds can be created that exclude subsets of jQuery functionality. This allows for smaller custom builds when the builder is certain that those parts of jQuery are not being used.
— jQuery
- Smaller builds download quicker, are parsed quicker and require less memory.
- Excluding methods best avoided restricts developers from using them (no more
$('p').css('red')
, etc).
Follow the official documention on creating a custom build to get you setup.
Then create your own custom build excluding modules you don't need:
grunt custom:-css,-css/showHide,-deprecated,-effects,-event/alias,-core/ready,-exports/amd
This custom build is an example that reflects the guidelines presented. Following this guide saves you 17KB (6kb gzipped) on your final jQuery size.
De Voorhoede waives all rights to this work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.