Question

DOM Ready in Vanilla JavaScript: DOMContentLoaded and Alternatives

javascriptjqueryhtml

Question

How can I run one or more standard JavaScript functions as soon as the HTML document is ready to be accessed and modified, without using jQuery's $(document).ready()?

For example, jQuery provides:

$(document).ready(function () {
  // Code that needs the DOM goes here.
});

What is the appropriate cross-browser approach in vanilla JavaScript? How does it differ from window.onload, an inline onload attribute on <body>, or calling a function from a script placed near the end of the page?

Short Answer

The vanilla JavaScript equivalent of jQuery's document-ready callback is usually the DOMContentLoaded event. It fires when the browser has parsed the HTML and built the DOM, so JavaScript can safely find and update page elements. You will also learn when to use load, how defer can remove the need for a ready listener, and how to handle code that may run after the DOM is already ready.

Concept

$(document).ready(...) is a jQuery convenience for waiting until the DOM is ready.

In modern browser JavaScript, listen for the DOMContentLoaded event:

document.addEventListener("DOMContentLoaded", function () {
  myFunction();
});

The DOM (Document Object Model) is the browser's JavaScript representation of the HTML page. Before the browser has parsed an element, code such as this may fail because the element does not exist yet:

const button = document.querySelector("#save-button");

DOMContentLoaded means:

  • The HTML has been parsed.
  • DOM elements are available to query and modify.
  • Deferred scripts have run.
  • Images, stylesheets, fonts, videos, and other external resources may still be loading.

This distinction matters because many page-initialization tasks need elements to exist, but do not need every image or external resource to finish downloading.

For nearly all supported modern browsers, DOMContentLoaded is the correct replacement for a jQuery ready callback.

Mental Model

Think of a web page as a building being assembled.

  • HTML parsing is the construction crew placing rooms, doors, and furniture into the building plan.
  • The DOM is the completed floor plan that JavaScript can inspect and change.
  • DOMContentLoaded is the announcement: “The building layout is complete; you can start arranging things.”
  • window.load is the later announcement: “Everything has arrived, including paintings, appliances, and decorations.”

If your code only needs to find a button and attach a click handler, wait for the layout (DOMContentLoaded), not every decorative resource (load).

Take Quiz

Syntax and Examples

Use addEventListener to register a callback for DOMContentLoaded:

document.addEventListener("DOMContentLoaded", function () {
  console.log("The DOM is ready.");
});

An arrow function works too:

document.addEventListener("DOMContentLoaded", () => {
  const message = document.querySelector("#message");
  message.textContent = "JavaScript started after the DOM was ready.";
});

Given this HTML:

<p id="message">Loading...</p>

the callback can safely select #message because the browser has finished parsing the document before it runs.

A reusable whenDomReady helper

Sometimes a script may be loaded after DOMContentLoaded has already fired. Check document.readyState so the callback runs in both cases:

function whenDomReady(callback) {
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", callback, { once: true });
  } else {
    callback();
  }
}

whenDomReady(() => {
  console.log("Safe to use the DOM.");
});

document.readyState can be:

  • "loading": HTML parsing is still in progress.
  • "interactive": the DOM has been parsed.
  • "complete": the page and its dependent resources have finished loading.

If the state is not "loading", the DOM is already usable.

Step by Step Execution

Consider this page:

<!doctype html>
<html lang="en">
  <head>
    <script>
      document.addEventListener("DOMContentLoaded", () => {
        const status = document.querySelector("#status");
        status.textContent = "Ready";
      });
    </script>
  </head>
  <body>
    <p id="status">Waiting...</p>
  </body>
</html>

Execution sequence:

  1. The browser starts parsing the HTML.
  2. It reaches the <script> in <head>.
  3. The script registers a DOMContentLoaded listener, then finishes. It does not try to select #status yet.
  4. The browser continues parsing and creates <p id="status"> in the DOM.
  5. HTML parsing finishes.
  6. The browser dispatches DOMContentLoaded.
  7. The callback runs, finds #status, and changes its text to Ready.

If the script called document.querySelector("#status") immediately in the <head>, it would run before the browser had parsed the <p> element. The result would be null.

Real World Use Cases

Use DOM-ready logic when your code depends on page elements being present:

  • Form behavior: attach submit, input, or change listeners after locating a form.
  • Navigation menus: initialize a mobile-menu button and its panel.
  • Client-side validation: find required fields and show validation messages.
  • Dashboard widgets: populate tables, charts, counters, or filters after their containers exist.
  • Accessibility enhancements: set focus, update ARIA attributes, or add keyboard handlers.
  • Progressive enhancement: turn server-rendered HTML into tabs, accordions, modals, or interactive components.

Use the load event instead when the actual dimensions or full availability of resources are required. For example, an image-processing script may need an image to finish loading before reading its natural size.

Take Quiz

Real Codebase Usage

In current projects, developers often avoid a global “ready” wrapper by loading scripts with defer:

<script src="app.js" defer></script>

A deferred external script runs after HTML parsing is complete, so app.js can usually access DOM elements directly:

const menuButton = document.querySelector("#menu-button");
const menu = document.querySelector("#menu");

menuButton.addEventListener("click", () => {
  menu.hidden = !menu.hidden;
});

Common project patterns include:

  • Component initialization: find all matching elements and initialize each one.

    document.querySelectorAll("[data-tabs]").forEach((tabsElement) => {
      initializeTabs(tabsElement);
    });
    
  • Guard clauses: skip setup when a component is not present on the current page.

    const searchForm = document.querySelector("#search-form");
    if (!searchForm) {
      // This page does not contain the search form.
    } else {
      initializeSearch(searchForm);
    }
    
  • Event delegation: attach one listener to an existing parent rather than attaching listeners to every child.

    document.addEventListener("click", (event) => {
      if (event.target.matches("[data-dismiss]")) {
        event.target.closest(".alert")?.remove();
      }
    });
    
  • Async data loading: wait for the DOM before displaying fetched API data in an existing container.

For new code, prefer external JavaScript files and addEventListener over inline attributes such as <body onload="...">.

Common Mistakes

Selecting elements too early

This script is in <head>, but the button appears later in <body>:

const button = document.querySelector("#save-button");
button.addEventListener("click", save);

button may be null, causing an error. Fix it by using DOMContentLoaded, adding defer to the external script, or placing a non-deferred script after the relevant HTML.

Using window.onload for DOM-only work

window.addEventListener("load", initializeMenu);

This works, but it waits for images and other resources too. For normal DOM setup, prefer:

document.addEventListener("DOMContentLoaded", initializeMenu);

Overwriting another handler

window.onload = initializeMenu;
window.onload = initializeAnalytics;

The second assignment replaces the first one. Use addEventListener so multiple listeners can coexist:

window.addEventListener("load", initializeMenu);
window.addEventListener("load", initializeAnalytics);

Using the wrong jQuery selector

The jQuery form is $(document).ready(...), not $('document').ready(...). The latter looks for an HTML element named document.

Assuming a late listener will fire

If a dynamically loaded script adds a DOMContentLoaded listener after the event has already happened, its callback will never run. Use a readyState check, such as the whenDomReady helper shown earlier.

Relying on inline event attributes

<body onload="myFunction()">

This mixes markup and behavior, is harder to maintain, and makes it less convenient to add independent handlers. Prefer JavaScript with addEventListener.

Comparisons

ApproachRuns whenBest useNotes
document.addEventListener("DOMContentLoaded", fn)The HTML has been parsed into a DOMInitializing elements and event handlersStandard replacement for jQuery ready
<script defer src="app.js"></script>After HTML parsing, before DOMContentLoaded completesLoading application scriptsOften removes the need for a ready listener
Script at the end of <body>After the HTML before that script has been parsedSmall pages or simple scriptsWorks, but defer often keeps HTML cleaner
window.addEventListener("load", fn)The full page and dependent resources are loadedResource-dependent workUsually later than needed for DOM setup
<body onload="fn()">Generally when the page load completesLegacy/simple markupAvoid in maintainable code; mixes HTML and JavaScript

DOMContentLoaded versus load

DOMContentLoaded answers: “Can I access the HTML elements yet?”

load answers: “Has the entire page, including dependent resources, finished loading?”

Take Quiz

Cheat Sheet

// Standard DOM-ready listener
document.addEventListener("DOMContentLoaded", initialize);

function initialize() {
  // Query and modify DOM elements here.
}
<!-- Preferred way to load an external app script -->
<script src="app.js" defer></script>
// Safe even if the DOM may already be ready
function whenDomReady(callback) {
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", callback, { once: true });
  } else {
    callback();
  }
}
// Use only when all page resources must be finished
window.addEventListener("load", () => {
  // Image-size-dependent code, for example.
});

Rules:

  • Use DOMContentLoaded for normal DOM initialization.
  • Use defer for external scripts that should run after parsing.
  • Use addEventListener, not window.onload = ..., when multiple handlers may exist.
  • Use load only when the full resource load matters.
  • Check document.readyState if a script can load late.

FAQ

What is the vanilla JavaScript equivalent of $(document).ready()?

Use document.addEventListener("DOMContentLoaded", callback). It runs the callback after the HTML has been parsed into a DOM.

Is DOMContentLoaded the same as window.onload?

No. DOMContentLoaded occurs after HTML parsing. load occurs later, after dependent resources such as images have loaded.

Do I need DOMContentLoaded when my script uses defer?

Usually no. A deferred external script runs after document parsing, so DOM elements are available. You may still use a listener if it makes the initialization timing clearer.

Can I put a script at the bottom of the body instead?

Yes. A script after the elements it uses can access those elements. However, an external script with defer is often easier to organize and keeps script-loading behavior explicit.

Why does my DOMContentLoaded callback not run?

It may have been registered after the event already fired. Check document.readyState; if it is not "loading", call the initialization function immediately.

Can more than one DOMContentLoaded handler be added?

Yes. Each addEventListener("DOMContentLoaded", callback) call adds another listener.

Do images delay DOMContentLoaded?

Images generally do not need to finish loading before DOMContentLoaded fires. If your logic depends on loaded images, use an image-specific load listener or the window load event.

Take Quiz

Mini Project

Description

Build a small notification banner that is initialized only after its HTML exists. The banner can be dismissed with a button, demonstrating safe DOM selection, event listeners, and a reusable DOM-ready helper.

Goal

Create a dismissible page notification that works whether the initialization script runs before or after the DOM becomes ready.

Requirements

Use a whenDomReady function that checks document.readyState.

Select the notification and its dismiss button after the DOM is ready.

Hide the notification when the user clicks the dismiss button.

Do not use inline onclick or onload attributes.

Avoid errors if the notification is not present on a page.

Take Quiz

Keep learning

Related questions

View more