Document: DOMContentLoaded event

Baseline Widely available

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

The DOMContentLoaded event fires when the HTML document has been completely parsed, and all deferred scripts (<script defer src="…"> and <script type="module">) have downloaded and executed. It doesn't wait for other things like images, subframes, and async scripts to finish loading.

DOMContentLoaded does not wait for stylesheets to load, however deferred scripts do wait for stylesheets, and the DOMContentLoaded event is queued after deferred scripts. Also, scripts which aren't deferred or async (e.g., <script>) will wait for already-parsed stylesheets to load.

A different event, load, should be used only to detect a fully-loaded page. It is a common mistake to use load where DOMContentLoaded would be more appropriate.

Usually, to avoid running a script before the DOM it manipulates has been fully constructed, you can simply place the script at the end of the document body, immediately before the closing </body> tag, without wrapping it in an event listener.

This event is not cancelable.

Syntax

Use the event name in methods like addEventListener().

js
addEventListener("DOMContentLoaded", (event) => { })

Note: There is no onDOMContentLoaded event handler property for this event.

Event type

A generic Event.

Examples

Basic usage

js
document.addEventListener("DOMContentLoaded", (event) => {
  console.log("DOM fully loaded and parsed");
});

Delaying DOMContentLoaded

html
<script>
  document.addEventListener("DOMContentLoaded", (event) => {
    console.log("DOM fully loaded and parsed");
  });

  for (let i = 0; i < 1_000_000_000; i++);
  // This synchronous script is going to delay parsing of the DOM,
  // so the DOMContentLoaded event is going to launch later.
</script>

Checking whether loading is already complete

Sometimes your script may run after the DOMContentLoaded event has already fired. This typically happens when the script runs asynchronously. Common scenarios include:

  • A module that is dynamically imported after the document is already loaded.
  • A script that is included via <script async>.
  • A script that is dynamically injected into the page.
  • Code that resumes after an asynchronous operation, such as await fetch(...), including after a top-level await in a module.

In these cases, you should check the document's readyState before adding a DOMContentLoaded listener, or your setup logic may not execute at all. For synchronous scripts (without async) that are already present in the initial markup, this situation does not occur. The document waits for the script to execute before firing DOMContentLoaded, so you are always sure that setup logic in the listener will be executed.

Consider the following script file in isolation:

js
function doSomething() {
  console.info("DOM loaded");
}

if (document.readyState === "loading") {
  // Loading hasn't finished yet
  document.addEventListener("DOMContentLoaded", doSomething);
} else {
  // `DOMContentLoaded` has already fired
  doSomething();
}

The script can't enforce how it's included by the HTML. If it's included via <script async>, or it's dynamically injected, then by the time it executes, DOMContentLoaded has already fired. To ensure that doSomething() always runs when the script loads, we need to have two paths, one that immediately runs doSomething if the document is already loaded, and another that runs doSomething once the document is loaded.

Note: There's no race condition here — it's not possible for the document to be loaded between the if check and the addEventListener() call. JavaScript has run-to-completion semantics, which means if the document is loading at one particular tick of the event loop, it can't become loaded until the next cycle, at which time the doSomething handler is already attached and will be fired.

Note: document.readyState is set to "interactive" after the completion of the HTML parser but before the execution of scripts with defer or type="module". DOMContentLoaded is fired after the execution of these scripts, but before the execution of scripts with async. document.readyState is set to "complete" after the execution of async scripts. This means that during the execution of deferred and module scripts, document.readyState is "interactive" but it's still possible to attach DOMContentLoaded listeners and make them fire as usual. In practice, executing doSomething() a little earlier is fine unless it relies on some global state set up by other deferred/module scripts.

Live example

HTML

html
<div class="controls">
  <button id="reload" type="button">Reload</button>
</div>

<div class="event-log">
  <label for="eventLog">Event log:</label>
  <textarea
    readonly
    class="event-log-contents"
    rows="8"
    cols="30"
    id="eventLog"></textarea>
</div>

JavaScript

js
const log = document.querySelector(".event-log-contents");
const reload = document.querySelector("#reload");

reload.addEventListener("click", () => {
  log.textContent = "";
  setTimeout(() => {
    window.location.reload(true);
  }, 200);
});

window.addEventListener("load", (event) => {
  log.textContent += "load\n";
});

document.addEventListener("readystatechange", (event) => {
  log.textContent += `readystate: ${document.readyState}\n`;
});

document.addEventListener("DOMContentLoaded", (event) => {
  log.textContent += "DOMContentLoaded\n";
});

Result

Specifications

Specification
HTML
# stop-parsing

Browser compatibility

desktop mobile
Chrome
Edge
Firefox
Opera
Safari
Chrome Android
Firefox for Android
Opera Android
Safari on iOS
Samsung Internet
WebView Android
WebView on iOS
DOMContentLoaded event
Chrome – Full support
Chrome 1 (Release date: 2008-12-11)
footnote Full support
Edge – Full support
Edge 12 (Release date: 2015-07-29)
footnote Full support
Firefox – Full support
Firefox 1 (Release date: 2004-11-09)
footnote Full support
Opera – Full support
Opera 9 (Release date: 2006-06-20)
footnote Full support
Safari – Full support
Safari 3.1 (Release date: 2008-03-18)
footnote Full support
Chrome Android – Full support
Chrome Android 18 (Release date: 2012-06-27)
footnote Full support
Firefox for Android – Full support
Firefox for Android 4 (Release date: 2011-03-29)
footnote Full support
Opera Android – Full support
Opera Android 10.1 (Release date: 2010-11-09)
footnote Full support
Safari on iOS – Full support
Safari on iOS 2 (Release date: 2008-07-11)
footnote Full support
Samsung Internet – Full support
Samsung Internet 1 (Release date: 2013-04-27)
footnote Full support
WebView Android – Full support
WebView Android 4.4 (Release date: 2013-12-09)
footnote Full support
WebView on iOS – Full support
WebView on iOS 2 (Release date: 2008-07-11)
footnote Full support

Legend

Tip: you can click/tap on a cell for more information.

Full support
Full support

See also