JavaScript in HTML: How the Script Is Wired to the Page

Lesson 01 got JavaScript running in the console and from a file. This lesson connects the two worlds: how a <script> element embeds or loads your code inside an HTML page, when the browser runs it, and how that code reaches into the page and changes it. After this lesson you can build and run a small interactive page — the setup for every lesson that follows.

A page is three cooperating files

HTML, CSS and JavaScript, in one loading step

A web page is not one file but a small system. The HTML file is the entry point: the browser reads it top to bottom, builds the DOM (the page as a tree of objects), loads the stylesheets and scripts it references, and renders. CSS files describe appearance. JavaScript files contain the behaviour — and the only connection between a page and its behaviour is the <script> element.

Diagram: an index.html file contains an h1 hook and a script tag that loads app.js; the browser engine runs it top to bottom, and it updates the page or writes to the console

The HTML declares what exists (including a hook: id="greeting"); the script the browser loads reads and changes it.

The contract: declare, then react

Keep this contract in mind for the whole track: HTML declares what exists; JavaScript reacts and updates. Code that quietly expects an element to exist is the source of the most common beginner error — you will meet it below, on purpose.

The <script> element

A minimal document with a script

This is a complete, working HTML page with JavaScript in it. Save it as first.html, open it in your browser, and open the console:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My first scripted page</title>
</head>
<body>
  <h1 id="title">Hello</h1>

  <script>
    // From this line on, the content is JavaScript, not HTML.
    console.log("The page says hi");
  </script>
</body>
</html>

You should see in the console: The page says hi, and the page shows a big "Hello" heading — which the script did not draw; the HTML did.

If you get this instead: nothing in the console — check that the tag is spelled <script> (not <scripts>) and that you saved the file before opening or reloading it.

Inline or external file?

JavaScript can live inside the HTML (above) or in its own file loaded with src. Both run identically; the difference is organization:

<!-- inline: fine for a tiny, page-specific snippet -->
<script>
  console.log("inline");
</script>

<!-- external: the normal choice for anything real -->
<script src="app.js"></script>

External files win for three reasons: the same code can serve many pages; the browser caches the file and stops re-downloading it; and HTML stays readable. The one rule that trips beginners is the path in src: src="app.js" means same folder as the HTML file; src="js/app.js" means a subfolder named js; src="/app.js" means from the root of the site — that one silently fails when you open the file from disk instead of a server.

Placement and loading order

The classic beginner error

The browser runs a script the moment it reaches it, while still reading the HTML. So this page fails:

<head>
  <script>
    // The browser has not built the body yet — there is no #title!
    const title = document.querySelector("#title"); // null
    title.textContent = "Hello";                    // TypeError!
  </script>
</head>

You should see in the console:

Uncaught TypeError: Cannot set properties of null (setting 'textContent')

Read it with the habit from lesson 01: the value was null ("nothing"), and you cannot set a property on nothing. The cause is timing, not spelling: the script ran before the element existed. There are three fixes, and one of them is the everyday default.

defer, async, module — what the timeline means

Timeline diagram: a plain script in the head blocks HTML parsing during download and execution; defer downloads in parallel and runs after parsing; async runs as soon as it downloads

A plain script pauses the whole page while it downloads and runs. defer downloads in parallel and runs after parsing; async runs whenever it is ready, in unpredictable order.

How you load itWhen it runsUse it when
plain <script>immediately, blocking the parseralmost never, in the head
<script src defer>after HTML is parsed, in orderdefault choice for app code
<script src async>as soon as downloaded, any orderindependent widgets, analytics
<script type="module">like defer, plus importsmulti-file apps (lesson 13)

Demo foundations/06_defer_vs_async.html demonstrates the difference live — predict the console order before you preview it.

Connecting code to the page

Selecting an element

document is the live page, provided by the browser. Its most used method, querySelector, takes any CSS selector and returns the first matching element — or null if there is none:

const title = document.querySelector("#title");   // by id — the usual case
const warning = document.querySelector(".warning"); // by class
const firstList = document.querySelector("ul");     // by tag

If you get null: the selector matched nothing. Check spelling (#titel finds nothing quietly) and check timing — the section above.

Reading and writing content

const title = document.querySelector("#title");

title.textContent = "Hello, JavaScript"; // write: replace the text, safely
console.log(title.textContent);          // read: what does it say now?

textContent treats everything as plain text — it is the safe default. Its cousin innerHTML parses the string as HTML, which is powerful for your own markup and dangerous with anything a user typed (it is how sites get hacked; stay on textContent until lesson 14).

Reacting to the user

The page should respond when something happens. You hand the browser a function and the event that triggers it:

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

// "when a click happens on this button, run this function"
button.addEventListener("click", function () {
  console.log("The button was clicked!");
});

Nothing runs as you write this line — the function is stored and runs later, once per click. This "run it later" pattern is why functions exist, and lesson 08 makes them solid.

A complete example: a click counter

<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Counter</title></head>
<body>
  <button id="increment">Clicked 0 times</button>
  <p id="milestone">Click the button.</p>

  <script>
    let count = 0; // state: remembers the count between clicks

    const button = document.querySelector("#increment");

    button.addEventListener("click", function () {
      count = count + 1;
      button.textContent = "Clicked " + count + " times";

      if (count === 10) {
        document.querySelector("#milestone").textContent = "Ten!";
      }
    });
  </script>
</body>
</html>

You should see: the button label climbing with every click, and the paragraph changing at ten. Read it again and name each part: state (count), wiring (addEventListener), updating the page (textContent), and one condition. Demo foundations/08_click_counter.html is this exact file, with more comments.

Where the output goes

The page is the product; the console is diagnostics

You now have two output channels, and choosing between them is a real skill: changes the user should see go to the page (textContent and friends); observations for you, the developer, go to the console. The console family is worth knowing early:

console.log("normal progress notes");
console.warn("something looks off, but it still works");
console.error("something failed");
console.table(["a", "b", "c"]); // arrays and objects, as a sortable table

When nothing seems to happen

The three checks, in order: (1) is the console open at all? (2) is there a red error — read its line number; (3) did your file load — add a console.log("loaded") as the first line of the file, and if it never appears, the src path is wrong. Ninety percent of "my JavaScript does not work" is one of these three.

When file:// is not enough

Why the browser refuses file://

Opening an HTML file from disk gives it the address scheme file://. Plain scripts work fine that way, but two features refuse it for security reasons: type="module" scripts and fetch (network requests). The console tells you unmistakably — you will see a CORS or Access to script … has been blocked error.

Run a local server

The fix is a tiny local web server, which serves your folder over http://localhost:

python -m http.server 8000   # then open http://localhost:8000/first.html

In VS Code, the free Live Server extension does the same with a click and reloads the page when you save. From here on, the track's module demos expect a local server; everything else previews straight from disk.

Practice: wire a page

The task

From the Lab Examples page, preview foundations/04_script_inline.html, foundations/05_script_external.html, and foundations/08_click_counter.html; then predict the console order of foundations/06_defer_vs_async.html before previewing it. Finally, rebuild the counter from an empty file without looking at the demo.

The checklist

  • You can explain why the head-script example reads null — and name two fixes.
  • You know which loading attribute is the everyday default (defer) and why.
  • You can select an element, change its text, and react to a click, from memory.
  • You know when file:// stops working and how to run a local server.
  • You rebuilt the counter from scratch and it worked — or you debugged it with the three checks above.