jQuery vs Vanilla JS in 2026: Migration, Risks, and When to Keep It

by Kuligaposten Sep 15 2026

What jQuery still gives you that the platform does not, a practical migration guide, and the real risks of ripping it out of a legacy codebase.

jQuery vs Vanilla JS in 2026: Migration, Risks, and When to Keep It

jQuery is old enough to have a driver's license in most countries, and the reason it's still in millions of <script> tags isn't inertia alone — for a long time it genuinely solved real problems. Cross-browser DOM quirks, inconsistent event handling, XMLHttpRequest's rough edges. Almost all of that is now just... how the platform works. querySelectorAll, fetch, classList, Promise, arrow functions — these used to be jQuery's pitch. Now they're baseline.

That doesn't mean every jQuery codebase should be rewritten this quarter. It means the calculus has changed, and it's worth being deliberate about which side of it you're on.

What jQuery actually gave you (and what's now native)

jQueryVanilla equivalent
$('.item')document.querySelectorAll('.item')
$('#id')document.getElementById('id') or document.querySelector('#id')
$el.on('click', fn)el.addEventListener('click', fn)
$el.css('color', 'red')el.style.color = 'red'
$el.addClass('x') / .removeClass('x')el.classList.add('x') / .remove('x')
$el.html('...') / .text('...')el.innerHTML = '...' / el.textContent = '...'
$.ajax(...) / $.get(...)fetch(...)
$el.each(fn)elements.forEach(fn) (on a NodeList/array)
$(document).ready(fn)document.addEventListener('DOMContentLoaded', fn), or just defer the script

The gap that's left isn't really about capability anymore — it's about ergonomics. A few examples worth looking at directly.

Event delegation

jQuery made delegated events trivial:

$(document).on("click", ".item", function () {
  console.log("clicked", this);
});

The vanilla version needs a manual check against the event target:

document.addEventListener("click", (event) => {
  const item = event.target.closest(".item");
  if (item) console.log("clicked", item);
});

Not hard, but it's a pattern you have to actually know — Element.closest() — rather than getting it for free.

AJAX error handling

This one trips people up during migrations. $.ajax treats a 404 or 500 as an error automatically:

$.get("/api/data")
  .done((data) => console.log(data))
  .fail((err) => console.error(err));

fetch does not. A 404 response is still a "successful" fetch as far as the Promise is concerned — you have to check response.ok yourself, or it'll silently try to parse an error page as JSON:

fetch("/api/data")
  .then((res) => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
  .then((data) => console.log(data))
  .catch((err) => console.error(err));

This is the single most common bug introduced in jQuery-to-fetch migrations: someone swaps $.ajax for fetch, forgets the res.ok check, and error responses start getting parsed as if they succeeded.

Animations

.fadeIn(), .slideUp(), .animate({...}) had no real native equivalent for a long time. Today, CSS transitions cover the common cases:

.panel {
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  transition:
    opacity 0.3s ease,
    max-height 0.3s ease;
}
.panel.open {
  opacity: 1;
  max-height: 500px;
}
panel.classList.toggle("open");

For anything more complex than opacity/transform/height, the Web Animations API (element.animate(keyframes, options)) covers most of what .animate() used to do, without the library.

The real risks of migrating

None of the above is the hard part. The hard part is what a migration touches that isn't obviously "jQuery code."

  • Plugins with no vanilla replacement. Old admin panels are full of jQuery-dependent datepickers, "chosen"-style select boxes, WYSIWYG editors, and drag-and-drop widgets. Ripping out jQuery means finding (or writing) a replacement for every one of these, not just rewriting your own $.ajax calls.
  • this binding differences. jQuery event handlers bind this to the element automatically. Plain addEventListener callbacks do the same for regular function handlers, but an arrow function callback keeps the outer this — a subtle, easy-to-miss regression if code gets converted mechanically.
  • Implicit iteration. $('.item').css('color', 'red') applies to every matched element without a loop. document.querySelectorAll('.item').style.color = 'red' doesn't work at all — querySelectorAll returns a NodeList, not a single element — and this is a common "why did it silently do nothing" bug during a rewrite.
  • Selector engine differences. jQuery's selector engine (Sizzle, historically) supported some non-standard CSS-like selectors and was more forgiving of edge cases than the native querySelector, which strictly follows the CSS spec.
  • Regression surface on code nobody's looked at in years. This is the big one. The risk of a jQuery migration usually isn't the migration itself — it's that touching old, stable, rarely-exercised code to remove a dependency reintroduces bugs that were fixed and forgotten about a decade ago.

When to actually keep jQuery

  • A large, stable legacy app where jQuery is deeply embedded and working. If it isn't causing a measurable problem (bundle size on a public-facing page, a security patch you can't get, hiring friction), a rewrite is often pure risk with no corresponding reward.
  • Heavy reliance on jQuery UI plugins with no maintained vanilla equivalent, where rebuilding the widget yourself is a bigger project than the migration was supposed to solve.
  • Internal tools where bundle size genuinely doesn't matter — an admin dashboard used by 12 people on a fast internal network isn't paying the same performance tax a public marketing site is.

When to migrate

  • Any new project. There's no good reason to add jQuery to a project started in 2026 — everything it offered as a starting point is either native now or better served by a framework if you need real UI state management.
  • Public, performance-sensitive pages. jQuery is roughly 30KB gzipped. On a marketing site or blog optimizing for Core Web Vitals, that's a real, measurable cost for very little benefit if you're only using it for a handful of $(...).on(...) calls.
  • Incrementally, not all at once. You don't need a big-bang rewrite. Stop writing new code in jQuery, and replace old usages opportunistically when you're already touching that file for another reason. A codebase can run jQuery and vanilla JS side by side indefinitely — they don't conflict.

The honest framing: jQuery isn't "bad" in 2026, it's just no longer necessary for most of what it used to be necessary for. The decision to migrate should be driven by an actual cost (bundle size on a page that matters, a plugin you can't maintain, a security concern) rather than by the fact that it feels dated. And if you do migrate, budget real time for the parts that aren't your own code — that's almost always where the risk actually lives.

Back to Home