Connecting HTML and JavaScript
Prerequisites: Connecting HTML and CSS
JavaScript makes pages interactive. It reaches the page through the DOM — the tree the browser built from your HTML.
Loading scripts
<head>
<meta charset="utf-8">
<title>Scripts</title>
<script src="/js/analytics.js" async></script>
<script src="/js/app.js" defer></script>
<script type="module" src="/js/widgets.js"></script>
</head>
| Script | Downloads | Runs |
|---|---|---|
<script src> (plain) |
Pauses parsing | Immediately — blocks the page |
async |
In parallel | As soon as it arrives — order not guaranteed |
defer |
In parallel | After the HTML is parsed, in document order |
type="module" |
In parallel | Deferred by default; supports import |
Use defer (or modules) for your app code and async for independent
scripts like analytics. A plain script in the head delays everything
below it.
Scripts run in order with parsing
A plain inline script sees only the HTML above it:
<p id="first">First</p>
<script>
console.log("first:", document.getElementById("first") !== null);
console.log("second:", document.getElementById("second") !== null);
document.addEventListener("DOMContentLoaded", () => {
console.log("after parsing, second:", document.getElementById("second") !== null);
});
</script>
<p id="second">Second</p>
first: true
second: false
after parsing, second: true
defer and modules avoid this problem — they run after the whole
document is parsed.
Finding and changing elements
<ul id="list">
<li>Rye</li>
<li class="special">Spelt</li>
</ul>
<p id="count"></p>
<script>
const list = document.querySelector("#list");
const items = list.querySelectorAll("li");
console.log(items.length, list.querySelector(".special").textContent);
const li = document.createElement("li");
li.textContent = "Sourdough <fresh>";
list.append(li);
console.log(list.lastElementChild.outerHTML);
document.getElementById("count").textContent = `${list.children.length} breads`;
console.log(document.getElementById("count").textContent);
</script>
2 Spelt
<li>Sourdough <fresh></li>
3 breads
Setting textContent inserts text, escaping < — the safe way to
show user data. innerHTML parses HTML and is dangerous with untrusted
input (Level 3, Sanitizing User Content).
Events
<button type="button" id="like" aria-pressed="false">Like</button>
<script>
const button = document.getElementById("like");
button.addEventListener("click", () => {
const pressed = button.getAttribute("aria-pressed") === "true";
button.setAttribute("aria-pressed", String(!pressed));
console.log("pressed:", button.getAttribute("aria-pressed"));
});
button.click();
button.click();
</script>
pressed: true
pressed: false
Prefer addEventListener over inline handler attributes like
onclick="..." — they mix behavior into markup and are blocked by a
strict Content Security Policy.
Progressive enhancement
Build the page so the HTML works on its own — real links, real forms that submit to the server — then let JavaScript improve it. If a script fails to load (it happens: flaky networks, blockers, errors), people can still use the page.
noscript content is shown only when scripting is disabled:
<noscript><p>The live map needs JavaScript. <a href="/directions">Read the directions instead</a>.</p></noscript>