Vanilla JS Equivalents to jQuery

Shuqi Khor
6 min readAug 22, 2023

--

Having used jQuery for more than half a decade, it’s not easy to migrate to other frameworks as the workflows are too different. The main thing that had me still clinging to jQuery sometimes is that it could be used out of the box without compiling, perfect for tiny projects.

But rewriting your codes to Vanilla JS is easy, and they work the same way. In most cases, you could free a page from jQuery within 15 minutes. Here’s how.

Selectors

Vanilla alternative to jQuery selectors

Selectors work basically the same as jQuery one, except if you try to select multiple elements at once:

// jQuery
$('.has-error').removeClass('has-error');

// Vanila - single
document.querySelector('.has-error').classList.remove('has-error');

// Vanilla - multiple
document.querySelectorAll('.has-error').forEach(element => {
element.classList.remove('has-error');
});

Converting NodeList to Array

When you use querySelectorAll(), you will get a NodeList, which you could iterate over using forEach().

But a NodeList isn’t exactly an array. If you want to apply array methods such as filter() or map(), you will have to convert it to an array first:

// The proper way
let productRows = Array.from(document.querySelectorAll('.product-row'));

// Shortcut - using spread operator
let productRows = [...document.querySelectorAll('.product-row')];

// Now you could apply array functions
productRows.filter(element => element.classList.contains('active'));

Traversing

Vanilla alternative to jQuery .find()

I don’t like to traverse a lot except downwards from the container. To find a nested element, just use querySelector() all over again:

/* -- find -- */

// jQuery
$(this).find('.btn-confirm').prop('disabled', true);

// Vanilla
this.querySelector('.btn-confirm').disabled = true;

Vanilla alternative to jQuery .closest()

When traversing upwards, it’s exactly the same as jQuery:

// jQuery
$(this).closest('tr').addClass('active');

// Vanilla
this.closest('tr').classList.add('active');

Vanilla alternative to jQuery .children()

Direct children are trickier to fetch, but :scope pseudo-class could help:

// jQuery
$(this).children('li').removeClass('d-none');

// Vanilla
this.querySelectorAll(':scope > li').forEach(
el => el.classList.remove('d-none')
);

Vanilla alternative to jQuery .next()

Traversing horizontally is even trickier. Actually you should avoid horizontal traversal altogether as your script could break when the DOM tree changes.

// jQuery
$(this).next('button').hide();

// Vanilla - https://stackoverflow.com/a/63715354
function nextMatch (el, selector) {
while (el = el.nextElementSibling) {
if (el.matches(selector)) return el;
}
}
nextMatch(this, 'button').style.display = 'none';

Manipulation

Vanilla alternative jQuery DOM manipulation methods

Manipulation is easy, just go back to the basic:

// .val() - set an input value
document.querySelector('input#hidden-data').value = JSON.stringify(data);

// .attr() - set a tag property
document.querySelector('#btn-submit').disabled = true;

// .css() - set a CSS property
document.querySelector('#status').style.color = 'red';

// .html() - replace HTML content
document.querySelector('#alert-text').innerHTML = "An error occurred.";

// .empty() - empty an element
document.querySelector('#canvas').textContent = ''; // you could also use innerHTML

// .remove() - remove an element
document.querySelectorAll('.row:empty').forEach(el => el.remove());

Vanilla alternative to jQuery insert methods

// .append() - append element to the end
document.querySelector('#table-users tbody').append(userRow);

// .before() - insert before an element
document.querySelector('#product-name').insertAdjacentHTML('beforebegin', '[NEW]');

// .after() - insert after an element
document.querySelector('#welcome').insertAdjacentHTML('afterend', `, ${username}`);

// .wrapAll() - wrap content inside an element
let container = document.createElement('nav');
let navItems = document.querySelectorAll('.nav-item');
navItems[0].parentNode.prepend(container);
container.append(...navItems);

Actually, these 3 methods are super useful as alternatives to before() and after():

They even have 4 positions to choose from: beforebegin, afterbegin, beforeend, afterend.

On the other hand, insertBefore() is useful too, but it’s more akin to append(), which made for the parent element.

Data Storage

Vanilla alternative to jQuery .data()

In jQuery you might come across .data() a lot, which could be used to store virtually any type of data though only accessible inside jQuery.

In Vanilla JS though, you could only store text-only data (or JSON encoded data). By setting a property in .dataset, the value will appear in the tag as data-* attribute:

// Plain text
document.querySelector('#modal').dataset.purpose = 'delete';

// JSON string
document.querySelector('#modal').dataset.info = JSON.stringify(data);

Theoretically, you could create a WeakMap to store any data in an element like jQuery, but it’s never a good practice.

In most cases, a couple of basic variables will be enough in place of data(). Better yet, try state management:

Event Listeners

Vanilla alternative to jQuery .click() event

Instead of shorthands like click(), submit() and scroll() in jQuery, you need to add event listeners in a more verbose way. But it’s not much of a difference though.

// jQuery
$('#btn-submit').click(function (e) {
$(this).html('Please wait...');
});

// Vanilla
document.querySelector('#btn-submit').addEventListener('click', function (e) {
this.innerHTML = 'Please wait...';
});

Vanilla alternative to jQuery $(document).on()

In jQuery, $(document).on() could listen for events from elements that haven’t even exist yet. This could be imitated by matching element selector with matches():

// jQuery
$(document).on('click', '.btn-remove', function (e) {
$(this).closest('.row').remove();
});

// Vanilla
document.addEventListener('click', function (e) {
if (e.target.matches('.btn-remove') || e.target.closest('.btn-remove') {
e.target.closest('.row').remove();
}
});

Notice how we also look for matches in its ancestors. This is important when, let’s say, you have an SVG icon inside your button.

Vanilla alternative to jQuery $(document).ready()

On top of that, it’s very common to use $(document).ready() in jQuery. The Vanilla alternative for it is the DOMContentLoaded event:

// jQuery
$(document).ready(function () {
/* codes here */
});

// Vanilla
function ready () {
/* codes here */
}
if (["interactive", "complete"].includes(document.readyState)) {
ready();
} else {
document.addEventListener('DOMContentLoaded', ready);
}

Here we also check the status of document.readyState, just in case that the DOM is already loaded.

But if you’re confident that your script would run before DOM is ready, you could place the function directly inside document.addEventListener().

CSS Classes

Vanilla alternative to jQuery class manipulation

CSS class manipulation is especially useful when you work with Bootstrap, therefore in our jQuery based projects, we have used hasClass(), addClass(), removeClass() and toggleClass() extensively.

Thankfully, Vanilla JS has had all the equivalents to these. They in fact have already been there for more than a decade:

// jQuery
$('#tab-details').removeClass('active');

// Vanilla
document.querySelector('#input-name').classList.add('text-danger', 'border');


// jQuery
$('#input-name').addClass('text-danger border');

// Vanilla
document.querySelector('#tab-details').classList.remove('active');


// jQuery
$('#input-other').toggleClass(
'd-block',
$('#select-reason').val() == 'other'
);

// Vanilla
document.querySelector('#input-other').toggleClass(
'd-block',
document.querySelector('#select-reason').value == 'other'
);


// jQuery
if ($('#btn-status').hasClass('btn-success')) {
$('#status').text('OK');
}

// Vanilla
if (document.querySelector('#btn-status').classList.contains('btn-success')) {
document.querySelector('#status').innerText = 'OK';
}

Vanilla JS even has a replace() method that jQuery doesn’t have:

document.querySelector('#icon').classList.replace('fa-caret-down', 'fa-caret-left');

AJAX

If you depend on $.ajax() a lot and it makes you feel difficult to leave jQuery, you could try Fetch API:

// From MDN - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
async function postData(url = "", data = {}) {
const response = await fetch(url, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
return response.json();
}
postData("https://example.com/answer", { answer: 42 })
.then(data => console.log(data));

Please note that PHP doesn’t accept JSON data right away, it only reads the x-www-form-urlencoded format. You will need to configure your PHP this way:

Alternatively, you could use popular AJAX libraries like Axios, but since the introduction of Fetch API, people are starting to ditch Axios as well.

Animation

jQuery provides a few animation methods that allow you to easily show and hide UI components in a pretty way. It runs pretty slow, but it works. In fact you may not even notice the performance problem in modern devices.

The two most-used methods are slideToggle() and fadeToggle():

// jQuery
$('#btn-toggle').click(function (e) {
$('#content').slideToggle();
});

$('#btn-toggle-chat').click(function (e) {
$('#chat-box').fadeToggle();
});

Vanilla ways to recreate jQuery slideToggle() and fadeToggle()

The modern way to do it is to let CSS handle the animation, which would be buttery smooth. Here’s an example on how to slide up and down using CSS:

And this is how to fade in and out using CSS keyframe animation:

For complex animations, I would recommend GSAP. It is said to be even faster than CSS animation in some circumstances.

With GSAP, you could plan animation timelines and have them run synchronously. On top of that, you could use it to create parallax scrolling website with its ScrollTrigger plugin.

That’s it!

--

--