---
# === IDENTITY ===
id: software/migrations/jquery-to-vanilla-js/2026
canonical_question: "How do I replace jQuery with modern vanilla JavaScript (ES6+)?"
aliases:
  - "jQuery to vanilla JavaScript guide"
  - "replace jQuery with native JS"
  - "remove jQuery from project"
  - "jQuery to ES6 migration"
  - "modern JavaScript without jQuery"
  - "you might not need jQuery"
  - "jQuery alternatives native JS"
  - "migrate jQuery to vanilla JS"
entity_type: software_reference
domain: software > migrations > jquery_to_vanilla_js
region: global
jurisdiction: global
temporal_scope: 2015-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.95
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "jQuery 4.0.0 (2026-01-17) — dropped IE10 and older, removed deprecated utility functions ($.isArray, $.trim, $.type, $.now, $.parseJSON, deferreds/callbacks), migrated from AMD to ES modules, switched from RequireJS to Rollup, added Trusted Types support"
  next_review: 2026-11-13
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Audit before migrating — run grep for all $( and jQuery( calls and count jQuery plugin dependencies BEFORE removing anything; a partial migration with jQuery still loaded is worse than no migration [src7]"
  - "Migrate jQuery plugins FIRST — plugins like Select2, Slick, DataTables hard-depend on jQuery; replace them with vanilla alternatives (Tom Select, Swiper, AG Grid) or their framework-native equivalents before removing jQuery [src1, src2]"
  - "querySelectorAll returns a static NodeList, not a live jQuery collection — you MUST iterate explicitly with forEach or spread into an array for .map()/.filter()/.reduce(); forgetting this is the #1 migration bug [src1, src3]"
  - "fetch() does NOT reject on HTTP errors (404, 500) — unlike $.ajax() which fires the error callback on 4xx/5xx, fetch resolves normally; always check response.ok before parsing the body [src4]"
  - "Arrow functions change this context — jQuery callbacks bind this to the DOM element; arrow functions inherit this from the enclosing scope; use function declarations or e.currentTarget for equivalent behavior [src7]"
  - "Third-party scripts may depend on window.jQuery — analytics, A/B testing tools (Optimizely, VWO), and CMS plugins (WordPress) often assume jQuery is globally available; audit ALL scripts, not just your own [src7]"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Migrating jQuery to React (component-based architecture, virtual DOM, JSX) — fundamentally different approach from vanilla JS replacement"
    use_instead: "software/migrations/jquery-to-react/2026"
  - condition: "Migrating jQuery to Vue (reactive data binding, single-file components) — different migration strategy than vanilla JS"
    use_instead: "software/migrations/jquery-to-vue/2026"
  - condition: "Building a complex SPA that needs state management, routing, and component lifecycle — vanilla JS is insufficient; consider a framework migration instead"
    use_instead: "software/migrations/jquery-to-react/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "codebase_size"
    question: "Approximately how many jQuery calls does the codebase have?"
    type: choice
    options: ["< 50 calls (small)", "50-200 calls (medium)", "200-1000 calls (large)", "> 1000 calls (very large)"]
  - key: "jquery_features_used"
    question: "Which jQuery features does the codebase primarily use?"
    type: choice
    options: ["selectors + DOM manipulation only", "selectors + AJAX", "selectors + events + AJAX", "jQuery UI (draggable, sortable, dialog)", "jQuery plugins (datepicker, slick, select2, etc.)", "full jQuery ecosystem"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/jquery-to-vanilla-js/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
  alternative_to:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React Migration"
    - id: "software/migrations/jquery-to-vue/2026"
      label: "jQuery to Vue 3 Migration"
  often_confused_with:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React (framework-based replacement, not vanilla JS)"
    - id: "software/migrations/jquery-to-vue/2026"
      label: "jQuery to Vue (framework-based replacement, not vanilla JS)"

# === SOURCES (8 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "You Might Not Need jQuery"
    author: HubSpot
    url: https://youmightnotneedjquery.com/
    type: community_resource
    published: 2024-01-15
    reliability: high
  - id: src2
    title: "You Don't Need jQuery -- GitHub Reference"
    author: Cam Song
    url: https://github.com/camsong/You-Dont-Need-jQuery
    type: community_resource
    published: 2024-06-10
    reliability: high
  - id: src3
    title: "Cheat Sheet for Moving from jQuery to Vanilla JavaScript"
    author: Tobias Ahlin
    url: https://tobiasahlin.com/blog/move-from-jquery-to-vanilla-javascript/
    type: technical_blog
    published: 2023-03-20
    reliability: high
  - id: src4
    title: "MDN Web Docs -- Using the Fetch API"
    author: Mozilla
    url: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
    type: official_docs
    published: 2025-08-20
    reliability: authoritative
  - id: src5
    title: "MDN Web Docs -- Web Animations API"
    author: Mozilla
    url: https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
    type: official_docs
    published: 2025-11-07
    reliability: authoritative
  - id: src6
    title: "MDN Web Docs -- Element.closest()"
    author: Mozilla
    url: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
    type: official_docs
    published: 2025-10-20
    reliability: authoritative
  - id: src7
    title: "Replacing jQuery with Vanilla ES6"
    author: Aaron T. Grogg
    url: https://aarontgrogg.com/blog/2021/09/29/replacing-jquery-with-vanilla-es6/
    type: technical_blog
    published: 2021-09-29
    reliability: moderate_high
  - id: src8
    title: "jQuery 4.0.0 Release"
    author: jQuery Foundation
    url: https://blog.jquery.com/2026/01/17/jquery-4-0-0/
    type: official_docs
    published: 2026-01-17
    reliability: authoritative
---

# How to Replace jQuery with Modern Vanilla JavaScript (ES6+)

## TL;DR

- **Bottom line**: Every jQuery method has a native browser equivalent in ES6+ -- `querySelector` replaces `$()`, `fetch` replaces `$.ajax()`, `classList` replaces `addClass/removeClass`, and the Web Animations API replaces `$.animate()`. With jQuery 4.0 (Jan 2026) dropping legacy browser support, the reasons to keep jQuery are fewer than ever. [src1, src2, src8]
- **Key tool/command**: `document.querySelector()` / `document.querySelectorAll()` -- the universal replacement for `$()`.
- **Watch out for**: `querySelectorAll` returns a static `NodeList`, not a live jQuery collection -- it does not have `.map()`, `.filter()`, or `.reduce()`, and it does not auto-update when the DOM changes. [src1, src3]
- **Works with**: All modern browsers (Chrome 60+, Firefox 55+, Safari 12+, Edge 79+). IE11 needs polyfills for `fetch`, `closest()`, and arrow functions, but jQuery 4.0 also dropped IE10 and older. [src1, src4, src8]

## Constraints

<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- **Audit before migrating** -- run `grep` for all `$(` and `jQuery(` calls and count jQuery plugin dependencies BEFORE removing anything; a partial migration with jQuery still loaded is worse than no migration. [src7]
- **Migrate jQuery plugins FIRST** -- plugins like Select2, Slick, DataTables hard-depend on jQuery; replace them with vanilla alternatives (Tom Select, Swiper, AG Grid) or their framework-native equivalents before removing jQuery. [src1, src2]
- **`querySelectorAll` returns a static `NodeList`, not a live jQuery collection** -- you MUST iterate explicitly with `forEach` or spread into an array for `.map()`/`.filter()`/`.reduce()`; forgetting this is the #1 migration bug. [src1, src3]
- **`fetch()` does NOT reject on HTTP errors (404, 500)** -- unlike `$.ajax()` which fires the error callback on 4xx/5xx, `fetch` resolves normally; always check `response.ok` before parsing the body. [src4]
- **Arrow functions change `this` context** -- jQuery callbacks bind `this` to the DOM element; arrow functions inherit `this` from the enclosing scope; use `function` declarations or `e.currentTarget` for equivalent behavior. [src7]
- **Third-party scripts may depend on `window.jQuery`** -- analytics, A/B testing tools (Optimizely, VWO), and CMS plugins (WordPress) often assume jQuery is globally available; audit ALL scripts, not just your own. [src7]

## Quick Reference

| jQuery Pattern | Vanilla JS Equivalent | Notes |
|---|---|---|
| `$('#id')` | `document.querySelector('#id')` | Returns single element or `null` [src1] |
| `$('.class')` | `document.querySelectorAll('.class')` | Returns static `NodeList`, not live collection [src1] |
| `$(el).find('.child')` | `el.querySelectorAll('.child')` | Scoped to element subtree [src2] |
| `$(el).closest('.parent')` | `el.closest('.parent')` | Walks up the DOM tree, returns first match or `null` [src6] |
| `$(el).parent()` | `el.parentElement` | Direct parent only [src2] |
| `$(el).siblings()` | `[...el.parentElement.children].filter(c => c !== el)` | Spread into array, filter self [src3] |
| `$(el).addClass('x')` | `el.classList.add('x')` | Also supports multiple: `.add('a','b')` [src1] |
| `$(el).removeClass('x')` | `el.classList.remove('x')` | Also supports multiple classes [src1] |
| `$(el).toggleClass('x')` | `el.classList.toggle('x')` | Returns `true` if class added, `false` if removed [src2] |
| `$(el).hasClass('x')` | `el.classList.contains('x')` | Returns boolean [src2] |
| `$(el).css('color','red')` | `el.style.color = 'red'` | For multiple: `Object.assign(el.style, {...})` [src3] |
| `$(el).attr('href')` | `el.getAttribute('href')` | For setting: `el.setAttribute('href', val)` [src1] |
| `$(el).data('key')` | `el.dataset.key` | Reads `data-key` attribute; auto-camelCases [src2] |
| `$(el).on('click', fn)` | `el.addEventListener('click', fn)` | No shorthand -- always explicit event name [src1] |
| `$(el).off('click', fn)` | `el.removeEventListener('click', fn)` | Must pass same function reference [src3] |
| `$(el).trigger('click')` | `el.dispatchEvent(new Event('click'))` | Use `CustomEvent` for custom data [src2] |
| `$(document).ready(fn)` | `document.addEventListener('DOMContentLoaded', fn)` | Or place `<script>` at end of `<body>` [src1] |
| `$(el).text()` | `el.textContent` | Getter and setter [src3] |
| `$(el).html()` | `el.innerHTML` | Getter and setter -- beware XSS [src3] |
| `$(el).val()` | `el.value` | For form elements [src2] |
| `$(el).append(child)` | `el.appendChild(child)` | Or `el.append(child)` (accepts strings too) [src1] |
| `$(el).prepend(child)` | `el.prepend(child)` | Accepts nodes and strings [src2] |
| `$(el).before(sibling)` | `el.before(sibling)` | Inserts before element [src2] |
| `$(el).after(sibling)` | `el.after(sibling)` | Inserts after element [src2] |
| `$(el).remove()` | `el.remove()` | Self-removing -- no parent reference needed [src1] |
| `$(el).clone(true)` | `el.cloneNode(true)` | `true` = deep clone including children [src2] |
| `$(el).show()` / `.hide()` | `el.style.display = ''` / `el.style.display = 'none'` | Or toggle a CSS class [src3] |
| `$(el).fadeIn()` | `el.animate([{opacity:0},{opacity:1}], 300)` | Web Animations API [src5] |
| `$(el).animate({...})` | `el.animate(keyframes, options)` | Web Animations API -- returns `Animation` object [src5] |
| `$.ajax({url, ...})` | `fetch(url, options)` | Returns `Promise` -- use `async/await` [src4] |
| `$.getJSON(url)` | `fetch(url).then(r => r.json())` | Always check `response.ok` [src4] |
| `$.each(arr, fn)` | `arr.forEach(fn)` | Or `for...of` for break support [src2] |
| `$.extend(a, b)` | `Object.assign(a, b)` | Or spread: `{...a, ...b}` (shallow only) [src3] |
| `$.inArray(val, arr)` | `arr.includes(val)` | Returns boolean (not index) [src2] |
| `$.isArray(x)` | `Array.isArray(x)` | Native since ES5; removed from jQuery 4.0 [src2, src8] |
| `$.map(arr, fn)` | `arr.map(fn)` | Native array method [src2] |
| `$.trim(str)` | `str.trim()` | Native string method; removed from jQuery 4.0 [src2, src8] |
| `$.proxy(fn, ctx)` | `fn.bind(ctx)` | Or use arrow functions for lexical `this` [src3] |
| `$.Deferred()` | `new Promise((resolve, reject) => {...})` | Native Promises -- no jQuery dependency. jQuery 4.0 slim removed Deferred entirely [src4, src8] |
| `$.when(p1, p2)` | `Promise.all([p1, p2])` | Also `Promise.race()`, `Promise.allSettled()`, `Promise.any()` [src4] |
| `$.Deferred()` with external resolve | `Promise.withResolvers()` | Returns `{promise, resolve, reject}` -- ES2024, Chrome 119+, FF 121+, Safari 17.4+ [src4] |
| `$.ajax({timeout})` | `AbortSignal.timeout(ms)` | `fetch(url, {signal: AbortSignal.timeout(5000)})` -- ES2024, Chrome 124+, FF 102+, Safari 17.4+ [src4] |
| `$.groupBy()` (never existed) | `Object.groupBy(arr, fn)` | Group array items by key -- ES2024, Chrome 117+, FF 119+, Safari 17.4+ [src4] |
| `$(items).filter(set1).filter(set2)` | `new Set(a).intersection(new Set(b))` | Native Set methods: `.union()`, `.intersection()`, `.difference()`, `.symmetricDifference()`, `.isSubsetOf()` -- ES2025, Chrome 122+, FF 127+, Safari 17+ [src4] |

## Decision Tree

```
START -- Replacing jQuery in an existing project
|-- Is jQuery loaded only for one or two features (e.g., AJAX + selectors)?
|   |-- YES -> Replace those specific calls with native equivalents from Quick Reference above
|   +-- NO |
|-- Is the codebase using jQuery plugins (datepicker, slick, select2)?
|   |-- YES -> Check if vanilla/framework alternatives exist for each plugin first
|   |         (e.g., Flatpickr, Swiper, Tom Select). Migrate plugins BEFORE removing jQuery.
|   +-- NO |
|-- Does the codebase use jQuery UI (draggable, sortable, dialog)?
|   |-- YES -> Replace with native HTML5 drag-and-drop, <dialog> element, or
|   |         lightweight libraries (SortableJS, interact.js). See Code Example 2.
|   +-- NO |
|-- Is the project a Single Page Application?
|   |-- YES -> Consider migrating to a framework (React, Vue, Svelte) instead of
|   |         vanilla JS -- they handle DOM updates more efficiently at scale.
|   |         See: jquery-to-react or jquery-to-vue units.
|   +-- NO |
|-- Does the codebase use $.ajax() extensively?
|   |-- YES -> Migrate AJAX calls first (fetch + async/await). See Step 3.
|   +-- NO |
|-- Is IE11 support required?
|   |-- YES -> Add polyfills for fetch, closest(), NodeList.forEach, Promise.
|   |         See Step 1 for polyfill setup. Note: jQuery 4.0 also drops IE10.
|   +-- NO |
+-- DEFAULT -> Follow the Step-by-Step Guide below for a systematic migration.
```

## Step-by-Step Guide

### 1. Audit jQuery usage and set up tooling

Identify every jQuery call in your codebase before changing anything. This determines your migration scope and reveals hidden plugin dependencies. [src7]

```bash
# Count total jQuery calls in your project
grep -r '\$(' src/ --include="*.js" | wc -l

# Find all unique jQuery methods used
grep -roP '\$\([^)]*\)\.\K[a-zA-Z]+' src/ --include="*.js" | sort | uniq -c | sort -rn

# Find jQuery plugin initializations
grep -rn '\.datepicker\|\.slick\|\.select2\|\.modal\|\.tooltip\|\.draggable\|\.sortable' src/ --include="*.js"

# Check which jQuery version is loaded
grep -r 'jquery' package.json bower.json *.html 2>/dev/null
```

**Verify**: Review the output -- if you see more than 200 jQuery calls, consider a phased migration (one module at a time) rather than a big-bang rewrite.

### 2. Replace DOM selection and traversal

This is the most common jQuery pattern -- typically 40-60% of all jQuery calls are selectors. [src1, src3]

```javascript
// BEFORE: jQuery selectors
const $header = $('#header');
const $items = $('.list-item');
const $active = $items.filter('.active');
const $parent = $header.closest('.container');

// AFTER: Vanilla JS selectors
const header = document.querySelector('#header');
const items = document.querySelectorAll('.list-item');
const active = document.querySelectorAll('.list-item.active');
const parent = header.closest('.container');

// Iterating over results -- jQuery auto-iterates, vanilla does not
// BEFORE:
$('.item').addClass('visible');

// AFTER -- option 1: forEach
document.querySelectorAll('.item').forEach(el => {
  el.classList.add('visible');
});

// AFTER -- option 2: reusable helper (recommended for large codebases)
const $$ = (selector, parent = document) => [...parent.querySelectorAll(selector)];
$$('.item').forEach(el => el.classList.add('visible'));
```

**Verify**: Open browser DevTools console, run `document.querySelectorAll('.your-class').length` -- should match what jQuery returned.

### 3. Replace AJAX calls with fetch

`fetch()` is the modern replacement for `$.ajax()`, `$.get()`, `$.post()`, and `$.getJSON()`. It uses Promises natively and supports `async/await`. [src4]

```javascript
// BEFORE: jQuery AJAX
$.ajax({
  url: '/api/users',
  method: 'GET',
  dataType: 'json',
  success: function(data) {
    console.log(data);
  },
  error: function(xhr, status, err) {
    console.error('Request failed:', err);
  }
});

// AFTER: fetch with async/await (recommended)
async function getUsers() {
  try {
    const response = await fetch('/api/users');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    const data = await response.json();
    console.log(data);
    return data;
  } catch (err) {
    console.error('Request failed:', err);
  }
}

// BEFORE: jQuery POST with form data
$.post('/api/users', { name: 'Alice', role: 'admin' }, function(resp) {
  console.log('Created:', resp);
});

// AFTER: fetch POST
async function createUser(name, role) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, role })
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
```

**Verify**: Check Network tab in DevTools -- request headers and payload should be identical to the jQuery version.

### 4. Replace event handling

Move from jQuery's `.on()` / `.off()` to native `addEventListener` / `removeEventListener`. Pay special attention to event delegation -- use `closest()` instead of `matches()` for reliable behavior with nested elements. [src1, src6]

```javascript
// BEFORE: jQuery events
$('#btn').on('click', function(e) {
  e.preventDefault();
  $(this).toggleClass('active');
});

// AFTER: Vanilla event binding
document.querySelector('#btn').addEventListener('click', function(e) {
  e.preventDefault();
  this.classList.toggle('active');
});

// BEFORE: jQuery event delegation (critical pattern)
$('#list').on('click', '.item', function() {
  $(this).addClass('selected');
});

// AFTER: Vanilla event delegation using closest()
document.querySelector('#list').addEventListener('click', function(e) {
  const item = e.target.closest('.item');
  if (!item) return;  // click was not on or inside .item
  item.classList.add('selected');
});

// BEFORE: jQuery document ready
$(document).ready(function() {
  init();
});
// or shorthand:
$(function() { init(); });

// AFTER: DOMContentLoaded (or place script at end of body)
document.addEventListener('DOMContentLoaded', () => {
  init();
});
```

**Verify**: Click the target elements -- behavior should be identical. Test with dynamically added elements to confirm delegation works.

### 5. Replace animations with Web Animations API or CSS transitions

The Web Animations API provides performant, cancelable animations without jQuery. For simple transitions, CSS is even better. The API runs on the compositor thread for better performance than jQuery's JavaScript-based animation loop. [src5]

```javascript
// BEFORE: jQuery animate
$('#panel').animate({ opacity: 0, height: 0 }, 400, function() {
  $(this).remove();
});

// AFTER -- Option A: Web Animations API
const panel = document.querySelector('#panel');
const animation = panel.animate(
  [
    { opacity: 1, height: panel.offsetHeight + 'px' },
    { opacity: 0, height: '0px' }
  ],
  { duration: 400, easing: 'ease-out', fill: 'forwards' }
);
animation.onfinish = () => panel.remove();

// AFTER -- Option B: CSS transition (simpler, often better)
// In CSS: .panel { transition: opacity 0.4s, height 0.4s; }
panel.style.opacity = '0';
panel.style.height = '0';
panel.addEventListener('transitionend', () => panel.remove(), { once: true });

// BEFORE: jQuery slideDown / slideUp
$('#menu').slideDown(300);

// AFTER: CSS class toggle
// CSS: .menu { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
// CSS: .menu.open { max-height: 500px; }
document.querySelector('#menu').classList.toggle('open');
```

**Verify**: Compare animation timing and smoothness side-by-side. Use `animation.finished` (a Promise) to chain sequential animations instead of jQuery's built-in queue.

### 6. Replace utility functions

jQuery utilities like `$.each`, `$.extend`, `$.inArray`, `$.trim`, and `$.type` all have native ES6+ equivalents. Note: jQuery 4.0 (Jan 2026) removed many of these utilities entirely. [src2, src3, src8]

```javascript
// BEFORE: jQuery utilities
var merged = $.extend({}, defaults, options);
var found = $.inArray('apple', fruits) !== -1;
var cleaned = $.trim(userInput);
var mapped = $.map(items, function(item) { return item.name; });
var isArr = $.isArray(data);
var type = $.type(value);

// AFTER: Native ES6+ equivalents
const merged = { ...defaults, ...options };           // or Object.assign({}, defaults, options)
const found = fruits.includes('apple');
const cleaned = userInput.trim();
const mapped = items.map(item => item.name);
const isArr = Array.isArray(data);
const type = typeof value;                            // or use instanceof for class checks
```

**Verify**: Run unit tests -- utility behavior should be identical. Watch for edge cases with `$.extend` deep merge vs `Object.assign` shallow merge.

### 7. Remove jQuery dependency

Once all jQuery calls are replaced, remove the library and verify nothing breaks. [src7]

```bash
# Remove jQuery from package.json
npm uninstall jquery

# Or remove from bower (legacy projects)
bower uninstall jquery

# Remove CDN script tags from HTML files
grep -rn 'jquery' *.html src/ templates/ --include="*.html"

# Search for any remaining $ or jQuery references
grep -rn '\$(\|jQuery(' src/ --include="*.js" --include="*.ts"

# Build and run tests
npm run build && npm test
```

**Verify**: Open the browser console -- there should be no `$ is not defined` or `jQuery is not defined` errors. Run full test suite.

## Code Examples

### JavaScript: Complete AJAX Migration with Error Handling and Retry

> Full script: [javascript-complete-ajax-migration-with-error-hand.js](scripts/javascript-complete-ajax-migration-with-error-hand.js) (82 lines)

```javascript
// Input:  Legacy jQuery AJAX calls scattered across codebase
// Output: Modern fetch wrapper with retry, timeout, and error handling
class ApiClient {
  constructor(baseUrl = '', defaultHeaders = {}) {
    this.baseUrl = baseUrl;
# ... (see full script)
```

### JavaScript: jQuery Plugin Replacement -- Delegated Event System

> Full script: [javascript-jquery-plugin-replacement-delegated-eve.js](scripts/javascript-jquery-plugin-replacement-delegated-eve.js) (66 lines)

```javascript
// Input:  jQuery-style event delegation (e.g., $(parent).on('click', '.child', fn))
// Output: Vanilla JS event delegation utility with namespace support
class EventDelegate {
  constructor(root = document) {
    this.root = typeof root === 'string' ? document.querySelector(root) : root;
# ... (see full script)
```

### JavaScript: Automated jQuery-to-Vanilla Codemod with jscodeshift

> Full script: [javascript-automated-jquery-to-vanilla-codemod-wit.js](scripts/javascript-automated-jquery-to-vanilla-codemod-wit.js) (37 lines)

```javascript
// Input:  jscodeshift transform file -- run against jQuery codebase
// Output: Automatically converts common jQuery patterns to vanilla JS
// Usage:  npx jscodeshift -t jquery-to-vanilla.js src/ --extensions=js
// File: jquery-to-vanilla.js (jscodeshift transform)
module.exports = function(fileInfo, api) {
# ... (see full script)
```

## Anti-Patterns

### Wrong: Using innerHTML for everything instead of proper DOM methods

```javascript
// BAD -- innerHTML is slow, destroys event listeners, and creates XSS vulnerabilities
const list = document.querySelector('#list');
list.innerHTML += `<li>${userInput}</li>`;  // Re-parses entire list, XSS risk
```

### Correct: Use DOM methods or sanitize input

```javascript
// GOOD -- createElement preserves existing listeners and prevents XSS
const list = document.querySelector('#list');
const li = document.createElement('li');
li.textContent = userInput;  // textContent auto-escapes HTML
list.appendChild(li);

// ALSO GOOD -- template literals with insertAdjacentHTML (sanitize first!)
const safeText = userInput.replace(/[<>&"']/g, c =>
  ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]));
list.insertAdjacentHTML('beforeend', `<li>${safeText}</li>`);
```

### Wrong: Using matches() instead of closest() for event delegation

```javascript
// BAD -- fails when clicking on child elements inside the target
document.querySelector('#list').addEventListener('click', (e) => {
  if (e.target.matches('.item')) {     // misses clicks on <span> inside .item
    e.target.classList.add('selected');
  }
});
```

### Correct: Use closest() for reliable event delegation

```javascript
// GOOD -- closest() walks up the tree and catches nested clicks [src6]
document.querySelector('#list').addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (!item) return;
  item.classList.add('selected');
});
```

### Wrong: Not checking response.ok with fetch

```javascript
// BAD -- fetch does NOT throw on HTTP errors (404, 500)
const data = await fetch('/api/users').then(r => r.json());
// This silently succeeds even on 404 -- you get an error page parsed as JSON
```

### Correct: Always check response.ok before parsing

```javascript
// GOOD -- explicit error handling for HTTP status codes [src4]
const response = await fetch('/api/users');
if (!response.ok) {
  throw new Error(`API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
```

### Wrong: Forgetting that querySelectorAll returns a NodeList, not an Array

```javascript
// BAD -- NodeList doesn't have .map(), .filter(), .reduce()
const items = document.querySelectorAll('.item');
const names = items.map(el => el.textContent);  // TypeError: items.map is not a function
```

### Correct: Convert NodeList to Array before using array methods

```javascript
// GOOD -- spread into array, or use Array.from() [src2, src3]
const items = [...document.querySelectorAll('.item')];
const names = items.map(el => el.textContent);

// ALSO GOOD -- Array.from with mapping function
const names2 = Array.from(
  document.querySelectorAll('.item'),
  el => el.textContent
);
```

### Wrong: Using Object.assign for deep merge (replacing $.extend(true, ...))

```javascript
// BAD -- Object.assign does shallow merge only
const defaults = { ui: { theme: 'light', fontSize: 14 } };
const userPrefs = { ui: { theme: 'dark' } };
const config = Object.assign({}, defaults, userPrefs);
// config.ui = { theme: 'dark' } -- fontSize is LOST!
```

### Correct: Use structuredClone or a deep merge utility

```javascript
// GOOD -- structuredClone (native since 2022) + custom deep merge [src3]
function deepMerge(target, ...sources) {
  for (const source of sources) {
    for (const key of Object.keys(source)) {
      if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
        target[key] = deepMerge(target[key] || {}, source[key]);
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}

const config = deepMerge({}, defaults, userPrefs);
// config.ui = { theme: 'dark', fontSize: 14 } -- both properties preserved
```

## Common Pitfalls

- **`querySelectorAll` returns a static NodeList**: Unlike jQuery collections, it does not auto-update when the DOM changes. If you add elements after the query, you must re-query. Fix: re-run `querySelectorAll()` after DOM mutations, or use `MutationObserver` for live tracking. [src1, src3]
- **`removeEventListener` requires the same function reference**: Anonymous functions cannot be removed. Fix: store handler functions in named variables -- `const handler = () => {...}; el.addEventListener('click', handler);` then `el.removeEventListener('click', handler)`. [src3]
- **`fetch` does not reject on HTTP errors**: A 404 or 500 response resolves the promise normally. Fix: always check `response.ok` or `response.status` before parsing the body. [src4]
- **`fetch` does not send cookies by default (cross-origin)**: Unlike `$.ajax` with `xhrFields: {withCredentials: true}`. Fix: add `credentials: 'include'` to the fetch options for cross-origin cookie requests. [src4]
- **`this` behaves differently in arrow functions**: Arrow functions inherit `this` from their enclosing scope, unlike jQuery callbacks where `this` is the DOM element. Fix: use a regular `function` declaration or access the element via the event parameter `e.currentTarget`. [src7]
- **No built-in `$.animate()` queue**: jQuery automatically queues animations; vanilla `element.animate()` plays them simultaneously. Fix: use `animation.finished` (a Promise) to chain animations -- `await el.animate(...).finished; el.animate(...)`. [src5]
- **`classList.toggle()` second argument gotcha**: `el.classList.toggle('active', condition)` adds/removes based on the boolean -- but older IE11 ignores the second argument. Fix: use `classList.add/remove` with a conditional if IE11 support is required. [src2]
- **jQuery's implicit iteration is gone**: `$('.items').hide()` hides all matched elements. Vanilla JS requires explicit looping: `document.querySelectorAll('.items').forEach(el => el.style.display = 'none')`. Forgetting this is the #1 source of bugs during migration. [src3]

## Diagnostic Commands

```bash
# Count remaining jQuery references in your codebase
grep -rn '\$(\|jQuery(' src/ --include="*.js" --include="*.ts" | wc -l

# Find jQuery CDN or local file references in HTML
grep -rn 'jquery\|jQuery' *.html templates/ --include="*.html"

# Check if jQuery is still in node_modules
ls node_modules/jquery/dist/jquery.min.js 2>/dev/null && echo "jQuery still installed" || echo "jQuery removed"

# Check bundle size impact (if using webpack/rollup)
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json

# Verify no runtime jQuery dependency (run in browser console)
# Should return "undefined" after successful migration
console.log(typeof jQuery, typeof $);

# Find files still importing jQuery via ES modules
grep -rn "import.*jquery\|require.*jquery" src/ --include="*.js" --include="*.ts"

# Run eslint to catch remaining $ usage (add to .eslintrc)
# "no-restricted-globals": ["error", { "name": "$", "message": "Use document.querySelector instead" }]
npx eslint src/ --rule '{"no-restricted-globals": ["error", {"name": "$", "message": "Use querySelector"}]}'
```

## Version History & Compatibility

| Technology | Version | Key APIs Available | Notes |
|---|---|---|---|
| ES6 / ES2015 | 2015+ | `let/const`, arrow functions, template literals, `Promise`, `class`, destructuring | Baseline for migration -- supported in all modern browsers [src1] |
| ES2017 | 2017+ | `async/await` | Eliminates callback-based AJAX patterns [src4] |
| `fetch` API | Chrome 42+, FF 39+, Safari 10.1+ | `fetch()`, `Request`, `Response`, `Headers`, `AbortController` | IE11: use `whatwg-fetch` polyfill. New: `fetchLater()` experimental API (2025+) [src4] |
| `closest()` | Chrome 41+, FF 35+, Safari 9+ | `Element.closest()` | Baseline since April 2017. IE11: use MDN polyfill [src6] |
| `classList` | Chrome 8+, FF 3.6+, Safari 5.1+ | `add()`, `remove()`, `toggle()`, `contains()`, `replace()` | IE11 lacks multi-argument support [src1] |
| Web Animations API | Chrome 36+, FF 48+, Safari 13.1+ | `Element.animate()`, `Animation`, `KeyframeEffect` | IE11/old Safari: use `web-animations-js` polyfill. `animation.finished` returns a Promise for chaining [src5] |
| `NodeList.forEach` | Chrome 51+, FF 50+, Safari 10+ | Iterating `querySelectorAll` results | IE11: use `Array.from()` workaround [src2] |
| `structuredClone` | Chrome 98+, FF 94+, Safari 15.4+ | Deep copy objects (replaces `$.extend(true,...)`) | Not in IE or older browsers. Cannot clone DOM nodes, functions, or `Error` objects [src2] |
| ES2024 | 2024+ | `Object.groupBy`, `Map.groupBy`, `Promise.withResolvers`, `AbortSignal.timeout`, `Array.fromAsync` | Eliminates more jQuery utility helpers. Baseline in Chrome 124+, FF 126+, Safari 17.4+ [src4] |
| ES2025 | 2025+ | Set methods: `.union()`, `.intersection()`, `.difference()`, `.symmetricDifference()`, `.isSubsetOf()`, `.isSupersetOf()`, `.isDisjointFrom()` | Native set algebra without jQuery `.filter().filter()` chains. Chrome 122+, FF 127+, Safari 17+ [src4] |
| jQuery 3.7.x | Current LTS | Full legacy API | Still maintained -- jQuery 3 EOL timeline TBD [src8] |
| jQuery 4.0.0 | Released 2026-01-17 | ES modules, Trusted Types, slim build (~19.5KB gzipped), Rollup packaging | Drops IE10 and older, Edge Legacy, older iOS/Android Browser. IE11 retained but planned for removal in 5.0. Removes deprecated utilities (`$.isArray`, `$.trim`, `$.type`, `$.now`, `$.parseJSON`, deferreds/callbacks in slim). Event handling now follows web standards more strictly [src8] |

## When to Use / When Not to Use

| Use Vanilla JS When | Keep jQuery When | Consider Instead |
|---|---|---|
| Starting a new project with modern browser targets | Maintaining a legacy app with extensive jQuery plugin ecosystem | React/Vue/Svelte for complex SPAs |
| Bundle size is critical (jQuery 4.0 slim is ~19.5KB gzipped) | Team velocity matters more than bundle size on internal tools | Preact (3KB) if you want React API without the size |
| You need only a few jQuery features (selectors, AJAX, events) | You need jQuery UI components and there is no time to replace them | Alpine.js or htmx for progressive enhancement |
| Building a library that should have zero dependencies | Supporting IE11 is a hard requirement and polyfill management is undesirable | Cash (~6KB) or Zepto (~10KB) as lightweight jQuery alternatives |
| Performance is critical (direct DOM access is faster than jQuery abstraction) | The codebase has 1000+ jQuery calls and no test coverage for safe refactoring | TypeScript migration first, then remove jQuery incrementally |

## Important Caveats

- **jQuery normalizes cross-browser behavior** -- vanilla JS may behave differently across browsers for edge cases like event properties, AJAX error handling, and animation timing. Always test in your target browser matrix after migration. [src1]
- **`$.extend(true, ...)` deep merge has no single native equivalent** -- `Object.assign` is shallow only. Use `structuredClone` for deep copies or implement a custom deep merge. `structuredClone` cannot clone DOM elements, functions, or `Error` objects. [src2]
- **jQuery's implicit iteration is gone** -- `$('.items').hide()` hides all matched elements. Vanilla JS requires explicit looping: `document.querySelectorAll('.items').forEach(el => el.style.display = 'none')`. Forgetting this is the #1 source of bugs during migration. [src3]
- **Removing jQuery may break third-party scripts** -- analytics snippets, A/B testing tools (Optimizely, VWO), and WordPress plugins often assume jQuery is globally available. Audit all scripts, not just your own code. [src7]
- **The `$` variable may conflict** -- other libraries (Prototype.js, MooTools, Cash) also use `$`. After removing jQuery, ensure no other library expects `window.$` to exist. [src2]
- **jQuery 4.0 removes deprecated utilities** -- if you are on jQuery 3.x and plan to upgrade to 4.0 instead of migrating to vanilla JS, note that `$.isArray`, `$.parseJSON`, `$.trim`, `$.type`, `$.now`, `$.isNumeric`, `$.isFunction`, `$.isWindow`, `$.camelCase`, and `$.nodeName` are all removed. Use their native equivalents. [src8]

## Related Units

- [jQuery to React Migration](/software/migrations/jquery-to-react/2026)
- [jQuery to Vue 3 Migration](/software/migrations/jquery-to-vue/2026)
- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
