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.