Question

JavaScript DOM Ready Without jQuery: $(document).ready Equivalent

javascriptjquerypageload

Question

I have a script that uses $(document).ready(...), but it does not use any other jQuery features. I would like to remove the jQuery dependency to make the script lighter.

How can I implement the equivalent of $(document).ready() using plain JavaScript?

I know that window.onload is not the same, because window.onload fires only after images, frames, and other external resources have fully loaded.

Short Answer

By the end of this page, you will understand what jQuery's $(document).ready() actually does, how to replace it with native JavaScript, and when to use DOMContentLoaded, document.readyState, or a deferred script instead.

Concept

$(document).ready() exists to run JavaScript as soon as the HTML document has been parsed and the DOM is ready to use.

In plain JavaScript, the native event for this is usually DOMContentLoaded.

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

This matters because many scripts need the DOM to exist before they can safely query elements, attach event listeners, or modify content.

For example, this code fails if it runs too early:

const button = document.getElementById('save');
button.addEventListener('click', handleSave);

If the browser has not parsed the <button id="save"> yet, button will be null.

DOMContentLoaded solves that by waiting until the document structure is loaded.

Important distinction

  • DOMContentLoaded: fires when the HTML has been parsed and the DOM tree is built
  • window.onload: fires later, after images, stylesheets, iframes, and other resources finish loading

In most cases, if you only need the DOM, DOMContentLoaded is the correct replacement for $(document).ready().

A small extra detail: if your script loads after the DOM is already ready, simply adding a DOMContentLoaded listener may do nothing. In that case, you should also check document.readyState. That is why many robust native replacements look like this:

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

This works whether the DOM is still loading or already ready.

Mental Model

Think of the browser as preparing a room before you enter it.

  • While the HTML is still being parsed, the furniture is still being placed.
  • When DOMContentLoaded fires, the room layout is finished and you can walk in and use it.
  • When window.onload fires, not only is the room ready, but all deliveries have also arrived: paintings, decorations, and extra equipment.

If your script only needs the room layout, you do not need to wait for every delivery. That is why DOMContentLoaded is usually the better choice.

Take Quiz

Syntax and Examples

Basic native replacement

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

This runs once the browser has finished parsing the HTML.

Reusable ready helper

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

ready(function () {
  const message = document.getElementById('message');
  if (message) {
    message.textContent = 'Page is ready';
  }
});

Why this helper is useful

If your script runs before the DOM is ready, it waits. If your script runs after the DOM is ready, it runs immediately.

Modern alternative: use defer

If your script is included in HTML, you can often avoid a ready wrapper entirely:

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

With defer, the browser downloads the script while parsing HTML, but executes it after the document has been parsed.

Then your JavaScript can often just be:

const form = document.getElementById('signup-form');
if (form) {
  form.addEventListener('submit', function (event) {
    event.preventDefault();
    console.log('Submitted');
  });
}

This is one of the cleanest modern approaches.

Step by Step Execution

Consider this code:

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

ready(function () {
  const title = document.getElementById('title');
  console.log(title.textContent);
});

Step by step

  1. The browser starts executing the script.
  2. The ready function is defined.
  3. ready(...) is called with a callback function.
  4. The code checks document.readyState.

Possible paths:

Case 1: DOM is still loading

  • document.readyState is 'loading'
  • The callback is attached to DOMContentLoaded
  • The browser continues parsing the HTML
  • When parsing finishes, DOMContentLoaded fires
  • The callback runs
  • document.getElementById('title') now finds the element safely

Case 2: DOM is already loaded

  • document.readyState is not 'loading'
  • The callback runs immediately
  • No event listener is needed

Why this is better than only using DOMContentLoaded

If you attach only this:

document.addEventListener('DOMContentLoaded', fn);

and your script is loaded after the event already fired, fn will never run.

The readyState check avoids that problem.

Real World Use Cases

Native DOM-ready logic is useful in many common situations:

  • Form setup: attach submit handlers after the form exists
  • UI initialization: open tabs, modals, menus, or accordions once elements are available
  • Client-side validation: register input listeners after fields are in the DOM
  • Dashboard widgets: render charts or counters into existing containers
  • Progressive enhancement: start with working HTML, then add JavaScript behavior when the DOM is ready
  • Third-party script cleanup: remove jQuery from older code that only used $(document).ready()

Example:

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

ready(function () {
  const menuButton = document.getElementById('menu-button');
  const menu = document.getElementById('menu');

  if (!menuButton || !menu) return;

  menuButton.addEventListener('click', function () {
    menu.classList.toggle('open');
  });
});
Take Quiz

Real Codebase Usage

In real projects, developers usually use one of these patterns:

1. Deferred scripts

This is common in modern codebases.

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

This often removes the need for a manual ready helper.

2. Guard clauses after DOM access

Developers often combine DOM-ready logic with safe checks:

document.addEventListener('DOMContentLoaded', function () {
  const modal = document.getElementById('modal');
  if (!modal) return;

  // Initialize modal here
});

This prevents errors on pages where an element does not exist.

3. Small init functions

Large applications often wrap startup code in named functions:

function initSearch() {
  const input = document.getElementById('search');
  if (!input) return;

  input.addEventListener('input', function () {
    console.log('Searching...');
  });
}

document.addEventListener('DOMContentLoaded', initSearch);

4. Feature-specific initialization

Instead of one giant ready block, developers split initialization by feature:

function initMenu() {}
function initFormValidation() {}
function initThemeToggle() {}

document.addEventListener('DOMContentLoaded', function () {
  initMenu();
  initFormValidation();
  initThemeToggle();
});

This keeps code easier to test and maintain.

Common Mistakes

1. Using window.onload when you only need the DOM

Broken approach for most DOM setup tasks:

window.onload = function () {
  console.log('Runs later than necessary');
};

Why it is a problem:

  • waits for images and other resources
  • can make initialization feel slower

Prefer:

document.addEventListener('DOMContentLoaded', function () {
  console.log('Runs as soon as the DOM is ready');
});

2. Forgetting that the event may have already fired

Broken code:

function runApp() {
  console.log('Start');
}

document.addEventListener('DOMContentLoaded', runApp);

If this script is loaded late, runApp may never run.

Safer version:

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

3. Accessing elements before they exist

Broken code:

const box = document.getElementById('box');
box.textContent = 'Hello';

If box is null, this throws an error.

Safer code:

document.addEventListener('DOMContentLoaded', function () {
  const box = document.getElementById('box');
  if (box) {
    box.textContent = 'Hello';
  }
});

4. Replacing existing window.onload handlers accidentally

Broken code:

window.onload = function () {
  console.log('First');
};

window.onload = function () {
  console.log('Second');
};

Only the second one survives.

Prefer event listeners:

window.addEventListener('load', function () {
  console.log('First');
});

window.addEventListener('load', function () {
  console.log('Second');
});

5. Adding a ready wrapper when defer already solves it

If your script tag uses defer, a separate ready helper may be unnecessary.

Comparisons

ApproachFires whenBest forNotes
$(document).ready()When DOM is readyOlder jQuery codejQuery abstraction
DOMContentLoadedWhen DOM is parsedNative DOM setupMain replacement
window.onloadAfter all resources loadCases that depend on images or full page assetsUsually later than needed
<script defer>Script runs after HTML parsingModern external scriptsOften the cleanest option
Script at end of <body>Usually after DOM elements are parsedSimple pagesWorks, but less explicit than defer

DOMContentLoaded vs load

  • Use DOMContentLoaded when you need HTML elements to exist.
  • Use load when you need images, styles, or other external assets fully loaded.

jQuery ready vs native ready helper

FeaturejQuery readyNative helper
Requires jQueryYesNo
Works after DOM is already readyYesYes, if using readyState check
LightweightNoYes
Modern recommendationUsually unnecessaryYes
Take Quiz

Cheat Sheet

// Basic replacement
document.addEventListener('DOMContentLoaded', function () {
  // DOM is ready
});

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

ready(function () {
  // Safe DOM code here
});

Quick rules

  • Use DOMContentLoaded to replace $(document).ready()
  • Use window.onload only if you need images or full assets loaded
  • Use document.readyState if the script might run after DOM readiness
  • Prefer <script defer> for modern external scripts
  • Check for missing elements with guard clauses

Common values

  • document.readyState === 'loading' → DOM still parsing
  • document.readyState === 'interactive' → DOM parsed
  • document.readyState === 'complete' → everything loaded

Good default choice

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

FAQ

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

Use document.addEventListener('DOMContentLoaded', callback).

Is DOMContentLoaded the same as window.onload?

No. DOMContentLoaded waits for the DOM. window.onload waits for all page resources.

Why might DOMContentLoaded not run in my script?

If your script loads after the event already fired, the listener will not run. Use a document.readyState check.

Can I avoid a ready handler completely?

Yes. If you load your script with defer, your code usually runs after the DOM is parsed.

Should I still use jQuery for document ready?

Usually no. Modern browsers support native DOM-ready patterns well.

When should I use window.onload instead?

Use it when your code depends on fully loaded images, iframes, or other external assets.

Is placing the script at the end of <body> enough?

Often yes for simple pages, because the DOM elements above it already exist. But defer is generally a cleaner modern approach.

Take Quiz

Mini Project

Description

Build a small page initializer that sets up a button click handler and updates a status message as soon as the DOM is ready, without using jQuery. This demonstrates how to safely work with DOM elements using a reusable native ready helper.

Goal

Create a plain JavaScript startup function that works whether the DOM is still loading or has already finished loading.

Requirements

  1. Create a reusable ready function using native JavaScript.
  2. Wait until the DOM is available before selecting elements.
  3. Update a status element when initialization completes.
  4. Add a click event listener to a button.
  5. Handle missing elements safely without throwing errors.
Take Quiz

Keep learning

Related questions

View more