jQuery to Modern JavaScript
A practical, no-nonsense guide to migrating from jQuery to modern vanilla JavaScript. When to do it, how to do it safely, and when to leave jQuery alone.
Is jQuery Still Tech Debt?
The honest answer is: it depends. jQuery is not inherently tech debt. It was the right tool for the job when browsers were a fragmented mess and the native DOM API was painful to work with. If your jQuery code is well-structured, well-tested, and doing its job, ripping it out just to say "we use modern JavaScript" is a waste of engineering time.
jQuery becomes tech debt when it blocks you from moving forward. When you cannot adopt a modern framework because jQuery conflicts with it. When your bundle is 90KB heavier than it needs to be and your mobile performance suffers. When new developers on your team have never written jQuery and it slows their onboarding. When the plugin ecosystem you depend on stops getting security patches.
The question is not "is jQuery bad?" The question is "is jQuery costing us more than it saves us?" If the answer is yes, it is time to migrate. If the answer is no, move on to problems that actually matter.
jQuery Is Fine When...
- Your site is mostly static content with light interactivity
- You are maintaining a legacy app in maintenance mode
- Your team knows jQuery well and ships reliably with it
- You do not need to support IE11 and are using jQuery 3.x (slim)
- Bundle size is not a concern for your use case
jQuery Is Tech Debt When...
- It conflicts with a modern framework you need to adopt
- Plugins you depend on are abandoned and unpatched
- You are loading jQuery just for two or three utility functions
- New hires spend weeks learning jQuery patterns they will never use again
- Security vulnerabilities in jQuery or its plugins go unpatched
When to Pull the Trigger on Migration
Migration is expensive. Do not start unless you have a clear reason. Here are the signals that the cost of keeping jQuery has exceeded the cost of removing it.
Bundle Size Is Hurting Performance
jQuery 3.7 minified and gzipped is roughly 30KB. The slim build is around 24KB. If you are building a performance-critical SPA or a mobile-first application where every kilobyte matters, that is a meaningful overhead for functionality that the browser now provides natively.
Modern Framework Adoption
React, Vue, and Svelte all manage the DOM themselves. Mixing jQuery's direct DOM manipulation with a virtual DOM framework creates race conditions, stale references, and debugging nightmares. If you are adopting a framework, jQuery has to go from those components.
Team Skills Have Shifted
If your last three hires all know ES6+ but none of them have written jQuery, your codebase is fighting your talent pool. Modern JavaScript is what bootcamps, courses, and computer science programs teach today. jQuery knowledge is increasingly rare in junior and mid-level developers.
Security Patches Are Ending
jQuery core still gets updates, but many popular jQuery plugins are abandoned. If you are using plugins that no longer receive security patches, you are carrying unpatched vulnerabilities. Every month you wait, the risk compounds.
Plugin Dependencies Are Blocking You
jQuery plugins often bundle their own version of jQuery or require specific versions. Version conflicts between plugins create a dependency maze. Modern alternatives use standard ES modules and are far easier to tree-shake and bundle.
You Are Already Rewriting Anyway
If a major rewrite or redesign is already happening, that is the cheapest time to drop jQuery. The code is already being touched. The tests are already being rewritten. Piggyback on existing change momentum instead of creating a separate migration project.
jQuery to Vanilla JS: Side-by-Side
The good news is that modern browsers have caught up with most of what made jQuery essential. Here is how every common jQuery pattern translates to native JavaScript that works in all modern browsers.
Selectors: $(selector) -> querySelector / querySelectorAll
// Single element
var el = $('#myId');
// Multiple elements
var items = $('.menu-item');
// Find within context
var link = $('#nav').find('a.active');
// First match
var first = $('li').first();// Single element
const el = document.querySelector('#myId');
// Multiple elements
const items = document.querySelectorAll('.menu-item');
// Find within context
const link = document.querySelector('#nav a.active');
// First match (querySelector returns first)
const first = document.querySelector('li'); Tip: querySelectorAll returns a NodeList, not an array. Use Array.from() or the spread operator [...nodeList] if you need array methods like map or filter. However, NodeList does support forEach natively.
AJAX: $.ajax / $.get / $.post -> fetch API
// GET request
$.ajax({
url: '/api/users',
method: 'GET',
dataType: 'json',
success: function(data) {
console.log(data);
},
error: function(xhr, status, err) {
console.error(err);
}
});
// POST request
$.post('/api/users', {
name: 'Jane',
role: 'dev'
}, function(response) {
console.log(response);
});// GET request
try {
const res = await fetch('/api/users');
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
// POST request
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Jane',
role: 'dev'
})
});
const response = await res.json(); Key difference: fetch does not reject on HTTP error status codes (404, 500). You must check response.ok manually. jQuery's $.ajax automatically calls the error handler for non-2xx responses.
Iteration: $.each -> forEach / map / filter
// Iterate over array
$.each(users, function(index, user) {
console.log(user.name);
});
// Iterate over jQuery collection
$('.item').each(function(i, el) {
$(el).addClass('processed');
});
// Map
var names = $.map(users, function(user) {
return user.name;
});// Iterate over array
users.forEach((user, index) => {
console.log(user.name);
});
// Iterate over NodeList
document.querySelectorAll('.item')
.forEach(el => el.classList.add('processed'));
// Map (spread NodeList to array if needed)
const names = users.map(user => user.name);
// Filter
const devs = users.filter(u => u.role === 'dev');Events: $(el).on -> addEventListener
// Click handler
$('#btn').on('click', function(e) {
e.preventDefault();
doSomething();
});
// Delegated event
$('#list').on('click', '.item', function() {
$(this).toggleClass('active');
});
// Remove handler
$('#btn').off('click', handler);
// One-time handler
$('#btn').one('click', function() {
console.log('Only fires once');
});// Click handler
document.querySelector('#btn')
.addEventListener('click', (e) => {
e.preventDefault();
doSomething();
});
// Delegated event
document.querySelector('#list')
.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) item.classList.toggle('active');
});
// Remove handler
btn.removeEventListener('click', handler);
// One-time handler
btn.addEventListener('click', () => {
console.log('Only fires once');
}, { once: true }); Event delegation: jQuery's delegated .on() is elegant. The native equivalent uses e.target.closest(selector) which is slightly more verbose but equally powerful and does not require loading a library.
Animation: $.animate -> CSS Transitions / Web Animations API
// Fade out
$('#panel').fadeOut(300);
// Slide toggle
$('#menu').slideToggle(400);
// Custom animation
$('#box').animate({
opacity: 0.5,
left: '+=50px',
height: 'toggle'
}, 600, 'swing', function() {
console.log('Animation done');
});/* CSS approach (preferred for simple animations) */
.panel { transition: opacity 0.3s ease; }
.panel.hidden { opacity: 0; }
// JS: just toggle the class
el.classList.toggle('hidden');
// Web Animations API (complex animations)
const anim = box.animate([
{ opacity: 1, transform: 'translateX(0)' },
{ opacity: 0.5, transform: 'translateX(50px)' }
], { duration: 600, easing: 'ease-in-out' });
anim.finished.then(() => {
console.log('Animation done');
}); Performance win: CSS transitions and animations run on the compositor thread, meaning they do not block the main thread. jQuery's $.animate runs on the main thread via setInterval, which causes jank on slower devices. This is one area where the modern approach is objectively better.
Document Ready: $(document).ready -> DOMContentLoaded / defer
// Classic form
$(document).ready(function() {
init();
});
// Shorthand
$(function() {
init();
});// Option 1: DOMContentLoaded event
document.addEventListener('DOMContentLoaded', () => {
init();
});
// Option 2: defer attribute (preferred)
// <script src="app.js" defer></script>
// Script runs after DOM is parsed - no wrapper needed
init(); Best practice: Use the defer attribute on your script tag instead of wrapping everything in a ready handler. The script loads in parallel with HTML parsing but executes only after the DOM is fully parsed. It is simpler, faster, and eliminates the need for any ready wrapper.
Object Merging: $.extend -> Object.assign / Spread
// Shallow merge
var config = $.extend({}, defaults, userOpts);
// Deep merge
var config = $.extend(true, {}, defaults, userOpts);
// Merge into existing object
$.extend(existingObj, newProps);// Shallow merge (Object.assign)
const config = Object.assign({}, defaults, userOpts);
// Shallow merge (spread - preferred)
const config = { ...defaults, ...userOpts };
// Deep merge (structuredClone + spread)
const config = {
...structuredClone(defaults),
...userOpts
};
// Merge into existing object
Object.assign(existingObj, newProps); Deep merge caveat: Neither Object.assign nor the spread operator does deep merging. If you need true deep merge (which $.extend(true, ...) provides), use structuredClone() for deep copying or a small utility like lodash's merge.
Migration Strategies
There is no single right way to remove jQuery. The best approach depends on your codebase size, team capacity, and risk tolerance. Here are three proven strategies, from aggressive to conservative.
Cold Turkey
Remove jQuery entirely in a single sprint. Replace every usage at once, test everything, and ship it.
Clean break, no mixed code
Fastest total timeline
High risk, large changeset
Requires significant test coverage
Best for: Small codebases (<5K lines of jQuery), strong test suites
Gradual Replacement
Convert one file or module at a time. jQuery and vanilla JS coexist during the transition. Every PR converts a piece.
Low risk per change
Can be done alongside feature work
Two coding styles coexist for months
Risk of stalling halfway
Best for: Medium codebases, teams with feature pressure, strangler fig pattern
Wrapper Pattern
Create a thin utility layer that mimics jQuery's API but uses vanilla JS under the hood. Swap the implementation, keep the interface.
Minimal code changes needed
Drop jQuery immediately, refactor later
Maintaining a custom abstraction
May delay learning native APIs
Best for: Large legacy codebases, quick bundle size wins
jQuery Plugin Replacements
The hardest part of dropping jQuery is often not jQuery itself - it is the plugin ecosystem. Here are modern, well-maintained replacements for the most common jQuery plugins.
| Category | jQuery Plugin | Modern Alternative | Notes |
|---|---|---|---|
| Date Picker | jQuery UI Datepicker | Flatpickr, Pikaday, native <input type="date"> | Native input works for simple cases; Flatpickr for custom styling |
| Modal / Dialog | jQuery UI Dialog, Bootstrap Modal | Native <dialog> element, Micromodal | The <dialog> element has 96%+ browser support and handles focus trapping |
| Carousel / Slider | Slick, Owl Carousel | Swiper, Splide, Embla Carousel | Swiper is framework-agnostic; Splide is lightweight at 29KB |
| Form Validation | jQuery Validate | Native Constraint Validation API, Pristine, just-validate | HTML5 validation handles 80% of use cases with zero JS |
| Autocomplete | jQuery UI Autocomplete, Select2 | Tom Select, Choices.js, native <datalist> | Tom Select is the spiritual successor to Select2 without jQuery |
| Drag and Drop | jQuery UI Sortable/Draggable | SortableJS, dnd-kit (React), native Drag and Drop API | SortableJS has no dependencies and works with any framework |
| Data Tables | jQuery DataTables | AG Grid, TanStack Table, Grid.js | AG Grid has a free community edition; TanStack is headless and framework-agnostic |
| Lightbox | Fancybox, Colorbox | GLightbox, PhotoSwipe, Spotlight.js | GLightbox is 12KB and supports video; PhotoSwipe is touch-optimized |
Testing During Migration
Migration without tests is just introducing new bugs while removing old dependencies. Here is how to test effectively throughout the process.
Write Tests Before You Migrate
Before converting any jQuery code, write end-to-end tests that capture the current behavior. Use Playwright, Cypress, or similar tools to test what the user sees - not implementation details. These tests should pass before and after your migration. If they break after converting, you introduced a regression.
Test Edge Cases jQuery Handled Silently
jQuery silently handles null elements, missing selectors, and empty collections. Vanilla JS throws errors. Test what happens when an element does not exist, when a selector matches nothing, and when an AJAX request fails. These silent failures are the most common source of post-migration bugs.
Test Across Browsers and Devices
One of the reasons jQuery existed was browser inconsistency. Modern browsers are much more consistent, but edge cases still exist - especially on mobile Safari and older Android WebViews. Run your test suite across Chrome, Firefox, Safari, and at least one mobile browser before declaring victory.
Measure Performance Before and After
Run Lighthouse or WebPageTest before you start and after each major conversion. Track bundle size, Time to Interactive, and First Contentful Paint. If performance is one of your reasons for migrating, you need data to prove the migration actually delivered the improvement you expected.
Framework or Vanilla? Choosing Your Target
Removing jQuery does not automatically mean you need React, Vue, or Svelte. The right choice depends on what you are building. Here is a decision framework.
Stay Vanilla When...
Your site is mostly content with light interactivity (forms, toggles, animations). You have fewer than 10 interactive components. You do not need client-side routing. You want zero build step complexity. Server-rendered pages with progressive enhancement are a great fit for vanilla JS.
Consider React When...
You are building a complex, stateful single-page application. Your UI has many components that share and synchronize state. You need a large ecosystem of third-party components. Your team already knows React or your hiring pool expects it.
Consider Vue When...
You want a gentle learning curve (especially coming from jQuery's imperative style). You need to progressively enhance an existing server-rendered app. Vue's template syntax and reactivity model feel familiar to developers who think in HTML and sprinkle in behavior.
Consider Svelte When...
You want the smallest possible bundle size. You prefer writing less boilerplate. You value compile-time optimization over runtime virtual DOM diffing. Svelte compiles to vanilla JS at build time, making it the closest to "vanilla with superpowers."
Related Resources
All Migration Guides
Browse all our technology migration guides with step-by-step strategies and real-world advice.
Proven Techniques
Strategies for tackling tech debt including refactoring patterns, dependency management, and more.
Strangler Fig Pattern
The gold standard for gradual migration - wrap, replace, and retire legacy code one piece at a time.
Frequently Asked Questions
Yes, and during a gradual migration you will have to. jQuery and vanilla JS coexist without issues as long as you follow two rules: do not let jQuery manage DOM elements that a framework (React, Vue) is also managing, and avoid mixing jQuery event handlers with addEventListener on the same element for the same event. The coexistence is temporary - set a deadline for full removal so you do not end up maintaining two approaches permanently.
This was jQuery's killer feature in 2006. In 2026, it is largely unnecessary. Every API discussed in this guide - querySelector, fetch, addEventListener, classList, CSS transitions, the Web Animations API - has 95%+ browser support including Safari and mobile browsers. The only exception is Internet Explorer, which Microsoft officially retired in June 2022. If you still need IE11 support, you have bigger problems than jQuery.
First, challenge whether you actually need the plugin. Many jQuery plugins solve problems that CSS or native HTML elements now handle (date pickers, dialogs, accordions). If you truly need the functionality, check if the plugin has been forked as a vanilla JS version. If not, isolate the plugin behind a wrapper function so the rest of your codebase does not depend on jQuery - only the wrapper does. You can then replace the wrapper's implementation later without touching the rest of your code.
It depends entirely on codebase size and jQuery usage depth. A small site (under 5,000 lines using jQuery) can be converted in one to two weeks by a single developer. A medium application (10,000 to 50,000 lines) typically takes two to four months with the gradual approach, done alongside feature work. A large legacy application with deep jQuery UI and plugin dependencies can take six to twelve months. The gradual approach is slower in total elapsed time but imposes less disruption on ongoing development.
Tools like jQuery Migrate can help identify deprecated API usage, which is useful for upgrading between jQuery versions. For converting to vanilla JS, tools like eslint-plugin-jquery can flag jQuery usage and suggest native alternatives. However, no automated tool can handle the nuances of event delegation patterns, plugin replacements, or animation conversions. Use tools for detection and tracking progress, but do the actual conversion manually with code review.
Starting the migration without clear success criteria and stopping halfway. Teams begin with enthusiasm, convert the easy parts, then hit a complex jQuery UI component or a deeply integrated plugin and stall. Six months later, the codebase has both jQuery and vanilla JS with no clear boundary between them - which is worse than having jQuery alone. Before you start, inventory every jQuery usage, estimate the effort for each, and commit to a timeline. If you cannot commit to finishing, do not start.
Ready to Modernize Your JavaScript?
Start with an inventory of your jQuery usage, pick a migration strategy, and convert one module at a time. Explore our techniques library for proven refactoring patterns.