Appearance
The Browser as a JavaScript Host
JavaScript in a browser uses the same language as JavaScript in Node, but receives a different set of host capabilities. document, location, history, fetch, localStorage, and browser events are Web APIs. They are not ECMAScript syntax, and many do not exist in a Node process.
The browser also imposes a lifecycle and security model. Code lives within a document from an origin, interacts with a user interface, shares a main thread with rendering, and may be restricted from reading resources belonging to other origins.
HTML decides how a script enters the page
An ordinary script executes in classic script mode:
html
<script src="legacy.js"></script>A module script uses ESM, has module scope and strict semantics, and is deferred by default:
html
<script type="module" src="/src/main.js"></script>Module imports in browsers use URLs. Relative imports need explicit paths and typically extensions:
js
import { mountDashboard } from "./dashboard.js";Bare package imports such as import React from "react" are not inherently resolved from node_modules by a browser. An import map or development/build tool maps them to browser-loadable URLs.
Module scripts wait for the document to be parsed, so code can usually find preceding page elements without listening for DOMContentLoaded. Code loaded by other mechanisms may need to account for document timing.
The DOM is an object model of the document
Given markup:
html
<main>
<h1 id="title">Transactions</h1>
<ul class="transaction-list"></ul>
</main>JavaScript can query and modify nodes:
js
const title = document.querySelector("#title");
const list = document.querySelector(".transaction-list");
if (!title || !list) {
throw new Error("Dashboard markup is incomplete");
}
title.textContent = "Recent transactions";querySelector returns the first match or null. The null check is a real runtime requirement, not only a TypeScript inconvenience.
Create DOM content with nodes and textContent when values come from users or services:
js
function renderTransaction(transaction) {
const item = document.createElement("li");
const amount = document.createElement("strong");
item.dataset.transactionId = transaction.id;
item.append(`${transaction.description}: `);
amount.textContent = formatCurrency(transaction.amount);
item.append(amount);
return item;
}
list.replaceChildren(...transactions.map(renderTransaction));Interpolating untrusted text into innerHTML can turn data into markup and script-capable content. innerHTML is useful when the markup is trusted and intentional, but it is not a general string-rendering shortcut.
DOM properties and HTML attributes are related but distinct. An input's value property reflects its current value, while its value attribute generally describes initial markup. Boolean properties such as checked are especially important to read from the element rather than by reconstructing attributes.
Events cross the environment boundary
js
const form = document.querySelector("#transaction-form");
form.addEventListener("submit", event => {
event.preventDefault();
const data = new FormData(form);
console.log(data.get("description"));
});preventDefault() cancels the browser's default action when the event is cancelable; it does not stop propagation. stopPropagation() affects the event's journey through ancestors and should be used sparingly.
Most events bubble from the target through ancestor elements. Delegation uses that fact to handle a changing collection with one listener:
js
list.addEventListener("click", event => {
const button = event.target.closest("button[data-action='delete']");
if (!button || !list.contains(button)) return;
deleteTransaction(button.dataset.transactionId);
});The listener remains stable even as list items are replaced. event.target is where the event began; event.currentTarget is the element whose listener is currently running.
Listeners are callbacks held by another object. Remove long-lived listeners when their owning UI is destroyed, using the same function reference:
js
function mountKeyboardShortcuts() {
function handleKeydown(event) {
if (event.key === "Escape") closeDialog();
}
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
}An abort signal can also group listener cleanup in supporting browsers.
Rendering happens around JavaScript
Changing the DOM marks work for the browser's rendering pipeline. The browser normally paints after JavaScript yields; repeated synchronous reads and writes of layout-sensitive properties can force expensive recalculation.
requestAnimationFrame schedules work before a future paint and is appropriate for visual updates:
js
function animate(timestamp) {
updatePosition(timestamp);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);It is not a replacement for every timer. Use it when work should track display frames, and ensure the work per frame stays small.
Browser code should treat accessibility as behavior, not decoration. Prefer semantic buttons, links, labels, and forms before recreating them with generic elements and event handlers. Keyboard behavior, focus, and assistive technology semantics come with native controls.
Global state has a cost
Top-level var in classic scripts can create properties on window; module bindings do not. Even with modules, the document and singleton browser APIs are shared mutable state. Pass required elements and capabilities into functions to make ownership clear:
js
export function mountTransactionList(root, { loadTransactions }) {
async function refresh() {
root.replaceChildren(renderLoading());
const transactions = await loadTransactions();
root.replaceChildren(...transactions.map(renderTransaction));
}
void refresh();
return { refresh };
}This small mounting pattern already contains ideas used by frameworks: an owned root, state-dependent rendering, event integration, and cleanup. Frameworks organize these ideas; they do not replace the browser beneath them.
Accessibility is behavior, not an annotation pass
Native controls supply keyboard behavior, focus semantics, form participation, and accessibility-tree information. Begin with a real form:
html
<form id="catalog-search" role="search">
<label for="catalog-query">Search the catalog</label>
<input id="catalog-query" name="query" type="search" required>
<button type="submit">Search</button>
<p id="search-status" aria-live="polite"></p>
</form>js
const form = document.querySelector("#catalog-search");
const status = document.querySelector("#search-status");
form.addEventListener("submit", event => {
event.preventDefault();
const data = new FormData(form);
const query = String(data.get("query") ?? "").trim();
if (!query) {
status.textContent = "Enter a search term.";
form.elements.query.focus();
return;
}
status.textContent = `Searching for ${query}…`;
});The label names the input, Enter submits it, and the live region announces changing status without moving focus. Specific error text should also be associated with its field, commonly through aria-describedby.
Dialogs, tabs, comboboxes, grids, and menus have more involved focus and keyboard contracts. Prefer native elements where their semantics fit or use a well-tested accessible component. Adding role="button" to a <div> does not add keyboard activation, focusability, disabled behavior, or form semantics.
A map of advanced browser APIs
The browser platform is much larger than the DOM and fetch. These APIs are worth recognizing, but their complete designs are deliberately outside this foundational course:
| Need | APIs | Required design knowledge |
|---|---|---|
| CPU work off the main thread | Web Workers, Shared Workers | Message ownership, structured cloning, termination, and error forwarding |
| Incremental data | Web Streams | Readers, cancellation, queuing, and whether the producer honors backpressure |
| Bidirectional server messages | WebSocket | Reconnection, authentication, ordering, heartbeats, and buffering |
| Peer media or data | WebRTC | Signaling, permissions, codecs, NAT traversal, and security |
| Offline applications | Service Workers, Cache Storage | Lifecycle updates, invalidation, quotas, and conflict policy |
| Structured client storage | IndexedDB | Transactions, schema evolution, indexes, and quotas |
| Same-origin context messaging | BroadcastChannel, postMessage | Origin checks, schemas, lifecycle, and duplicate delivery |
| Layout and visibility observation | ResizeObserver, IntersectionObserver | Delivery timing, feedback loops, thresholds, and cleanup |
These are host capabilities with independent lifecycle, compatibility, privacy, and performance rules. Learn one when a concrete requirement calls for it, check current support, and test its failure path. Related Node APIs are not necessarily interchangeable.
Optional prompts
Explain: What behavior does a native <button> provide that <div onClick=...> does not gain merely from a click handler?
Answer: A button is focusable, activates through expected keyboard input, exposes button semantics to assistive technology, participates correctly in forms, and supports disabled behavior. Re-creating that contract on a generic element requires several event, focus, and accessibility decisions and is rarely justified.
Debug: A ResizeObserver callback changes the observed element's width on every notification. Why can this become a feedback loop, and how should the design change?
Answer: Changing the width creates another resize, which schedules another notification and another change. Make the update converge to a stable result, observe a different boundary, or move the write outside the measurement relationship. Browsers may report a resize-loop warning, but that is not a substitute for removing the cycle.
Modify: The search input displays an error in <p id="query-error">. How should the input and message be connected, and when should focus move?
Answer: Add
aria-describedby="query-error"while the error applies so the input's description includes it. Move focus to the invalid control when submission fails and that movement helps recovery; do not move focus merely because validation text updates during every keystroke.