$(document).ready() without jQuery
The vanilla JavaScript replacement for jQuery's document ready: DOMContentLoaded, a readyState check for late scripts, and the defer attribute.
Listen for the DOMContentLoaded event on document. It fires once the HTML has been parsed and every element exists, which is the moment $(document).ready() and its shorthand $(function () { ... }) waited for.
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.querySelector('.menu-toggle');
const menu = document.querySelector('.menu');
toggle.addEventListener('click', () => {
menu.classList.toggle('is-open');
});
});When the event has already fired
DOMContentLoaded fires only once. If your code runs after that, for example from a script loaded with async, injected later by a tag manager, or pasted into the console, the listener never runs and nothing happens. jQuery handled this for you. Without it, check document.readyState first:
function ready(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
} else {
callback();
}
}
ready(() => {
console.log('The DOM is ready');
});readyState is 'loading' while the HTML is still being parsed. Once it's 'interactive' or 'complete', the elements are there, so the callback can run immediately.
Simplest: no wrapper at all
In most projects you don't need either of those. Load the script with defer, or as a module:
<head>
<script defer src="/js/app.js"></script>
<!-- or -->
<script type="module" src="/js/app.js"></script>
</head>A defer script downloads in parallel with the HTML and runs after parsing is done, right before DOMContentLoaded. Several deferred scripts run in the order they appear. Module scripts are deferred by default, and that includes inline ones. The defer attribute has no effect on an inline classic <script>.
async is different: the script runs as soon as it has downloaded, which can be before the rest of the HTML has been parsed. That's why an async script needs the ready() helper if it touches the DOM.
A plain <script> at the end of <body> also works, because everything above it has been parsed by then. It was the usual approach before defer was widely used.
DOMContentLoaded vs load
window.addEventListener('load', () => {
// Images, stylesheets and iframes have finished loading too
});The load event on window waits for every image, stylesheet and iframe, so on a heavy page it can fire seconds after DOMContentLoaded. Running setup code there makes menus and buttons feel broken until everything has downloaded. Use load only when you need something that depends on those resources, like the real size of an image, and use defer or DOMContentLoaded for everything else.