articles / Working with the DOM Without a Framework

Working with the DOM Without a Framework

Working with the DOM Without a Framework

Frameworks are useful, but a lot of interface work needs nothing more than the browser. The DOM API has improved steadily, and the methods that once required a helper library are now built in and consistent across browsers. Knowing the native API makes framework code easier to reason about and makes small projects far lighter.

Selecting elements

Two methods cover almost every case. querySelector returns the first element matching a CSS selector, and querySelectorAll returns all of them as a static list.

const submit = document.querySelector('#submit');
const items = document.querySelectorAll('.list-item');

items.forEach((el) => el.classList.add('ready'));

Because the selector is plain CSS, the same knowledge used for styling applies to scripting. There is rarely a reason to reach for the older getElementById family except for a small performance edge in hot loops.

Handling events

addEventListener attaches a handler and can be removed later, which is its main advantage over assigning to an onclick property. For a list of many similar elements, attaching one listener to a common ancestor and checking the target is more efficient than wiring up each child. This pattern is called event delegation.

document.querySelector('#list').addEventListener('click', (event) => {
  const button = event.target.closest('button[data-id]');
  if (!button) return;
  remove(button.dataset.id);
});

The handler runs once for the whole list. closest walks up from the clicked node to find the relevant button, and dataset reads the data-id attribute. New buttons added later work automatically because the listener lives on the parent, which also means there is no cleanup to do when items are removed. For events that fire rapidly, such as scroll or input, it is worth throttling or debouncing the handler so it does not run hundreds of times a second and stall the page.

Reading layout properties such as offsetHeight forces the browser to recalculate immediately. Group reads together, then group writes, so the browser is not thrashed into recalculating between every change.

Updating the page

To change content safely, set textContent rather than innerHTML whenever the value is plain text. textContent treats the value as text, so a string containing markup is shown literally instead of being parsed, which closes a common injection hole.

const el = document.createElement('li');
el.textContent = userSuppliedName;
el.classList.add('row');
list.append(el);

Building elements with createElement, setting properties, and calling append is verbose but explicit and safe. When many nodes are added at once, build them into a DocumentFragment first and append the fragment once, so the page reflows a single time instead of once per node.

Keeping it accessible

Direct DOM work makes it easy to forget the parts of an interface that are not visual. A clickable element built from a plain div is invisible to keyboard users and screen readers unless it is given a role, a tab index, and key handling. The simpler fix is almost always to use the right element in the first place: a button for actions, an a for navigation, a real label tied to each form field. They come with focus handling and keyboard behavior built in.

// prefer this
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Remove';

// over a div pretending to be a button

When content changes after load, it also helps to move focus deliberately, for example to a newly opened panel, so keyboard and screen-reader users are not left behind. These are small habits that cost little at build time and are expensive to retrofit later.

When to add a framework

The native API is a good fit for forms, small widgets, progressive enhancement, and pages where most content is rendered on the server. A framework starts to earn its weight when the interface holds significant client state that must stay in sync across many components, or when the same view logic is repeated in many places. Below that threshold, direct DOM code is smaller, faster to load, and has nothing to upgrade.

Common questions

Is querySelectorAll result a real array?

No, it returns a static NodeList. It has forEach, but to use array methods like map or filter, convert it first with Array.from or the spread operator.

What is event delegation?

Attaching a single event listener to a common ancestor instead of to each child element. The handler inspects event.target to decide what was interacted with. It is efficient and works for elements added after the listener is set up.

Why prefer textContent over innerHTML?

textContent treats its value as plain text, so any markup in the string is shown literally rather than parsed and executed. That avoids a common cross-site scripting risk when displaying user-supplied data.

Do I still need a library like jQuery for the DOM?

For most modern projects, no. The features that made jQuery essential, such as consistent selectors and event handling, are now built into browsers and behave the same across them.