Vanilla JavaScript Cookbook

A single reference for the small things you used to reach for jQuery to do. Every snippet here is dependency-free, modern vanilla JavaScript that runs in any current browser. Jump to what you need:

Run code when the DOM is ready

The replacement for $(document).ready(). This runs the callback immediately if the document has already parsed, and waits otherwise — so it works no matter where the script is placed.

function ready(fn) {
  if (document.readyState !== 'loading') {
    fn();
  } else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}

ready(() => {
  // your code here
});

Hide and show an element

Use the hidden property for a clean semantic toggle. Reach for style.display only when you need a specific display value back.

const box = document.querySelector('#box');

box.hidden = true;                 // hide
box.hidden = false;                // show
box.toggleAttribute('hidden');     // toggle

// When you need display control instead:
box.style.display = 'none';
box.style.display = '';            // revert to the stylesheet value

Check, add and toggle a class

The classList API covers everything hasClass, addClass and toggleClass did.

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

el.classList.contains('open');        // hasClass
el.classList.add('open');             // addClass
el.classList.remove('open');          // removeClass
el.classList.toggle('open');          // toggleClass
el.classList.toggle('open', isOpen);  // force on/off with a condition

Change the page title

One property. A common use is flashing a message when the tab loses focus.

document.title = 'New page title';

// Flash a message when the visitor switches tabs:
const original = document.title;
document.addEventListener('visibilitychange', () => {
  document.title = document.hidden ? 'Come back! 👋' : original;
});

Copy content from one element to another

Copy markup with innerHTML, or clone the actual nodes when you want to preserve structure without re-parsing a string.

const source = document.querySelector('#source');
const target = document.querySelector('#target');

// Copy the markup as a string:
target.innerHTML = source.innerHTML;

// Or move/clone the real nodes:
target.replaceChildren(...source.cloneNode(true).childNodes);

Build an HTML list from an array

Two ways: build real nodes (safe with untrusted text), or assemble a string (terser for trusted data).

const items = ['Alpha', 'Beta', 'Gamma'];

// Node-based — safe, no HTML injection:
const ul = document.createElement('ul');
ul.append(...items.map((text) => {
  const li = document.createElement('li');
  li.textContent = text;
  return li;
}));
document.querySelector('#list').replaceWith(ul);

// String-based — fine for trusted, escaped data:
const html = `<ul>${items.map((i) => `<li>${i}</li>`).join('')}</ul>`;

slideToggle without jQuery

If you used jQuery only for a couple of effects, this is the drop-in replacement for slideToggle(). The same three functions also give you slideUp() and slideDown(). It animates height, margin and padding with a CSS transition, then cleans the inline styles up afterwards.

const slideUp = (target, duration = 500) => {
  target.style.transitionProperty = 'height, margin, padding';
  target.style.transitionDuration = duration + 'ms';
  target.style.boxSizing = 'border-box';
  target.style.height = target.offsetHeight + 'px';
  target.offsetHeight; // force reflow
  target.style.overflow = 'hidden';
  target.style.height = 0;
  target.style.paddingTop = 0;
  target.style.paddingBottom = 0;
  target.style.marginTop = 0;
  target.style.marginBottom = 0;
  window.setTimeout(() => {
    target.style.display = 'none';
    ['height','padding-top','padding-bottom','margin-top','margin-bottom',
     'overflow','transition-duration','transition-property']
      .forEach((p) => target.style.removeProperty(p));
  }, duration);
};

const slideDown = (target, duration = 500) => {
  target.style.removeProperty('display');
  let display = window.getComputedStyle(target).display;
  if (display === 'none') display = 'block';
  target.style.display = display;
  const height = target.offsetHeight;
  target.style.overflow = 'hidden';
  target.style.height = 0;
  target.style.paddingTop = 0;
  target.style.paddingBottom = 0;
  target.style.marginTop = 0;
  target.style.marginBottom = 0;
  target.offsetHeight; // force reflow
  target.style.boxSizing = 'border-box';
  target.style.transitionProperty = 'height, margin, padding';
  target.style.transitionDuration = duration + 'ms';
  target.style.height = height + 'px';
  ['padding-top','padding-bottom','margin-top','margin-bottom']
    .forEach((p) => target.style.removeProperty(p));
  window.setTimeout(() => {
    ['height','overflow','transition-duration','transition-property']
      .forEach((p) => target.style.removeProperty(p));
  }, duration);
};

const slideToggle = (target, duration = 500) =>
  window.getComputedStyle(target).display === 'none'
    ? slideDown(target, duration)
    : slideUp(target, duration);

// Usage:
document.querySelector('#toggle').addEventListener('click', (e) => {
  e.preventDefault();
  slideToggle(document.querySelector('#toggleMe'), 300);
});

Make an iframe responsive

The modern answer is pure CSS — no JavaScript and no resize listener.

/* CSS */
.iframe-wrap { aspect-ratio: 16 / 9; }
.iframe-wrap iframe { width: 100%; height: 100%; border: 0; }

If you must size it from JavaScript (mixed ratios, legacy markup), keep it to one resize handler:

function resizeIframes() {
  document.querySelectorAll('iframe[data-ratio]').forEach((frame) => {
    const [w, h] = frame.dataset.ratio.split(':').map(Number);
    frame.style.height = `${frame.clientWidth * (h / w)}px`;
  });
}
window.addEventListener('resize', resizeIframes);
resizeIframes();

Centers across multi-monitor setups by accounting for the screen offset.

function openCenteredPopup(url, title, w, h) {
  const dualLeft = window.screenLeft ?? screen.left;
  const dualTop = window.screenTop ?? screen.top;
  const width = window.innerWidth || document.documentElement.clientWidth || screen.width;
  const height = window.innerHeight || document.documentElement.clientHeight || screen.height;
  const left = (width - w) / 2 + dualLeft;
  const top = (height - h) / 2 + dualTop;
  return window.open(url, title,
    `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
}

// Usage:
openCenteredPopup('https://example.com/share', 'share', 600, 480);

A tiny confirmation dialog

The native <dialog> element gives you a real modal — focus trapping and Escape-to-close included — with no library. This wrapper returns a promise that resolves to true or false.

function confirmDialog(message) {
  return new Promise((resolve) => {
    const dialog = document.createElement('dialog');
    dialog.innerHTML = `
      <form method="dialog">
        <p>${message}</p>
        <menu>
          <button value="cancel">Cancel</button>
          <button value="confirm">OK</button>
        </menu>
      </form>`;
    document.body.appendChild(dialog);
    dialog.addEventListener('close', () => {
      resolve(dialog.returnValue === 'confirm');
      dialog.remove();
    });
    dialog.showModal();
  });
}

// Usage:
if (await confirmDialog('Delete this item?')) {
  // proceed
}

Inject CSS rules at runtime

Add a block of CSS from JavaScript by creating a <style> element, or push a single rule into an existing sheet.

// A block of rules:
const style = document.createElement('style');
style.textContent = `.note { color: #c00; font-weight: 600; }`;
document.head.appendChild(style);

// A single rule into the first stylesheet:
const sheet = document.styleSheets[0];
sheet.insertRule('.note { color: #c00; }', sheet.cssRules.length);

Load an external stylesheet on demand

Append a <link> and resolve when it has loaded — handy for deferring non-critical CSS.

function loadStylesheet(href) {
  return new Promise((resolve, reject) => {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = href;
    link.onload = resolve;
    link.onerror = reject;
    document.head.appendChild(link);
  });
}

await loadStylesheet('/css/print.css');

Load a script on the fly

Inject a script and get a promise back, so you can await a third-party library before using it.

function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.async = true;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

await loadScript('https://cdn.example.com/widget.js');
// widget is now available

Read and update URL query parameters

URLSearchParams handles parsing and serialization. Pair it with history.replaceState to update the address bar without a reload.

const params = new URLSearchParams(location.search);

params.get('id');          // read
params.has('id');          // check
params.set('page', '2');   // add or update
params.delete('ref');      // remove

// Reflect changes in the URL without reloading:
history.replaceState(null, '', `${location.pathname}?${params}`);

localStorage with an expiry

localStorage never expires on its own. Wrap values with a timestamp to get cookie-style TTLs without cookies.

function setItem(key, value, ttlMs) {
  const record = { value, expiry: Date.now() + ttlMs };
  localStorage.setItem(key, JSON.stringify(record));
}

function getItem(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;
  const { value, expiry } = JSON.parse(raw);
  if (Date.now() > expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return value;
}

// Usage — remember a dismissed banner for one day:
setItem('bannerDismissed', true, 24 * 60 * 60 * 1000);

Defer (lazy-load) images

Native lazy-loading covers most cases with a single attribute:

<img src="photo.jpg" loading="lazy" alt="…">

For full control (placeholders, custom thresholds), swap a data-src in with an IntersectionObserver:

const io = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;
    const img = entry.target;
    img.src = img.dataset.src;
    observer.unobserve(img);
  });
}, { rootMargin: '200px' });

document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));

Fetch content from another URL

fetch replaces $.get / $.ajax for loading remote HTML or data.

async function getContent(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.text();   // or response.json()
}

const html = await getContent('/fragments/sidebar.html');
document.querySelector('#sidebar').innerHTML = html;

Cross-origin note: the browser will only let you read the response if the other site sends an Access-Control-Allow-Origin header that permits your domain. For sites that don’t, you need a small server-side proxy on your own domain to fetch on your behalf.

Set a global from inside a function

The old “my variable disappears outside the jQuery callback” problem is just scope. Don’t rely on implicit globals — attach to window explicitly, or better, keep a binding in module scope and avoid the global entirely.

// Explicit global (clear, but pollutes window):
function init() {
  window.appConfig = { ready: true };
}

// Preferred — module/outer scope, no global:
let appConfig;
function init() {
  appConfig = { ready: true };
}

// appConfig is now readable by any function in the same module

Navigate browser history (back, forward, reload)

I once had a shop client who needed customers to jump back several steps in one click while keeping their session intact. The browser’s history object handles this without any library — no jQuery required.

// Go back one entry (same as the browser's Back button)
history.back();

// Go back three entries in one go
history.go(-3);

// Go forward one entry
history.forward();

// Reload the current page
history.go(0);

You can wire these to a link with href="javascript:history.go(-3)", or attach them to a button’s click handler. history.go(n) accepts a positive or negative integer and moves that many entries through the history list (when the requested entry exists); these methods are supported in all major browsers.