Archives

MY APPROACH

The right solution, not the biggest solution.

You don't need to know whether your problem needs an API, an automation workflow, a dashboard or a custom application.

Tell me the problem. I'll help figure out a practical way to solve it.

Vanilla JS DOM Manipulation Without jQuery

You don’t need jQuery to toggle a modal, highlight a row, or stash an ID on a button. Modern browsers ship with everything required for fast, readable DOM work using just three APIs: querySelector, classList, and dataset. If you’re still reaching for $() in 2026, you’re paying a bundle tax for features you no longer need.moldstud+1

This isn’t about purity for purity’s sake. It’s about smaller bundles, fewer dependencies, and code that’s easier to reason about when you (or your future self) come back to it six months later.

You don't need jQuery to find an element, toggle a class, or read a data attribute. You haven't for about 8 years. If you're still writing $('.btn').addClass('active'), here's the replacement that ships with every browser.

The core swap

  • $('.item') -> document.querySelector('.item') / document.querySelectorAll('.item')
  • addClass / removeClass -> element.classList
  • attr('data-id') -> element.dataset

That's 90% of what you used jQuery for.

Vanilla JS DOM Manipulation: The 3 APIs You Actually Need

1. querySelector and querySelectorAll

This is your new workhorse. It uses CSS selectors, so your existing knowledge transfers directly.

querySelector tutorial in one line: querySelector returns the first match, querySelectorAll returns all of them as a static NodeList.

// single element
const modal = document.querySelector('#modal');

// multiple - works like you expect, but it's not an array
const buttons = document.querySelectorAll('.btn');

buttons.forEach(btn => {
  btn.addEventListener('click', handleClick);
});

// scoped search - only inside the card
const title = card.querySelector('.card__title');

I still forget that querySelectorAll doesn't have map or filter until my code blows up. Quick fix: [...buttons].filter(b => b.disabled).

2. classList - stop string-hacking className

If you've ever done el.className += ' active' and ended up with btn active active, you know the pain.

JavaScript classList is an actual API for it.

const el = document.querySelector('.menu');

el.classList.add('is-open');
el.classList.remove('is-hidden');
el.classList.toggle('is-open'); // returns true/false

// this is the one people miss - forced toggle
el.classList.toggle('is-open', shouldBeOpen);

// check
if (el.classList.contains('is-open')) {
  // ...
}

// replace - super handy for state swaps
el.classList.replace('is-loading', 'is-loaded');

No more regex, no more trimming spaces.

3. dataset - your HTML-to-JS bridge

JavaScript dataset reads data-* attributes and handles the camelCase conversion for you. data-user-id becomes dataset.userId. It also writes back to the DOM.

// <button data-user-id="42" data-action="delete">

const btn = document.querySelector('[data-action="delete"]');

console.log(btn.dataset.userId); // "42" - always a string
console.log(btn.dataset.action); // "delete"

// set it
btn.dataset.status = 'pending';
// -> <button data-status="pending">

// destructure for clean code
const { userId, action } = btn.dataset;

Production-grade pattern I use everywhere:

document.addEventListener('click', (e) => {
  const trigger = e.target.closest('[data-action]');
  if (!trigger) return;

  const { action, userId } = trigger.dataset;
  
  if (action === 'delete') {
    deleteUser(userId);
  }
});

One listener, no jQuery delegation, works for elements added later.

Real-World Gotchas

1. querySelectorAll is static, not live.
getElementsByClassName updates itself when the DOM changes. querySelectorAll doesn't. If you cache const items = document.querySelectorAll('.item') and then add a new .item via JS, items won't include it. Re-query or use event delegation with closest().

2. classList.toggle without a second arg is flaky in UI state.
toggle() flips whatever is there. If you have async code, you can get out of sync. Always use the forced boolean when your state comes from somewhere else: el.classList.toggle('is-active', isActive). It's deterministic.

3. dataset is always a string, and it camelCases.
data-user-id is dataset.userId, not dataset['user-id']. And if you store JSON, you have to parse it yourself: JSON.parse(el.dataset.config). I’ve spent 20 minutes debugging because I compared dataset.count === 2 instead of Number(dataset.count) === 2. Check your types.

What to do next

You don't need to rewrite everything. Next time you reach for jQuery for a toggle or a data attribute, use these three.

The code gets smaller, you drop 87kb, and you stop fighting a library that was solving IE6 problems. Browser APIs caught up.

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment

RDA
Rashid Anwar
Independent Developer

I’m Rashid, an independent developer focused on practical software solutions for businesses.

HAVE A PROJECT IN MIND?

Let's talk about the problem you're trying to solve.

Share what you need. We can discuss the requirements and a practical next step.

Start a Conversation