Web Programming Basics

This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve.

This section’s bibliography lists the reference material consulted while preparing these pages.

Client-side JavaScript exists to turn static HTML documents into interactive web applications. Every script that runs in a browser tab shares two objects that make this possible: window, the global object for that tab, and document, the root of the tree of nodes that represents the page currently on screen. This page covers those two objects, how to find and read the elements of a page, and how to create, insert, remove, and modify those elements — the foundation that every other browser API (events, CSS, geometry, storage, and so on) builds on.

The window and document Global Objects

There is exactly one global object per browser tab (or per <iframe>, if the page embeds one), and all non-module scripts running in that tab share it. This global object plays two roles at once: it is where JavaScript’s standard library lives (Math, Array, parseInt(), and so on), and it also represents the browser window itself, exposing properties like innerWidth, history (see Location, Navigation & History), and navigator. One of its own properties is named window, and its value is the global object itself — so window is simply how you refer to the global object explicitly:

window === globalThis;   // true in a browser tab -- window is the global object
window.innerWidth;       // the viewport width in pixels
innerWidth;              // the same value -- the window. prefix is optional but often clearer

Every window has a document property that refers to a Document object, which represents the currently displayed page and is the central object for reading and manipulating its content. HTML documents are trees: each HTML tag corresponds to an Element object, and each run of text corresponds to a Text object. Element, Text, and Document are all subclasses of the more general Node class, and the tree of Node objects is what the Document Object Model (DOM) API lets JavaScript query and modify.

document.head;   // the <head> Element
document.body;   // the <body> Element
document.URL;    // the page's own URL as a string

Tree terminology borrows from family trees: the node directly above another is its parent; the nodes directly below are its children; nodes with the same parent are siblings; and every node reachable by walking down from a node is one of its descendants.

var and top-level function declarations in a non-module script become properties of window, so function f() {} at the top level can be called as f() or window.f(). Top-level const, let, and class declarations do not become window properties, even though they are still shared by every non-module script in the document. Using ES modules avoids this shared global namespace entirely — each module has its own top-level scope.

Selecting Document Elements

Before a script can read or change part of a page, it has to obtain a reference to the relevant Element object(s). The modern, preferred way to do this is with CSS selector syntax — the same syntax used in stylesheets (tag names, #id, .class, attribute selectors, and combinators like >, +, and ~).

Method Returns Notes

document.querySelector(selector)

The first matching Element, or null

Stops searching as soon as one match is found.

document.querySelectorAll(selector)

A NodeList of every matching Element

Static — a snapshot taken at call time; later document changes do not affect it.

document.getElementById(id)

A single Element, or null

The id argument is a bare id, without the # prefix.

document.getElementsByTagName(tag) / getElementsByClassName(cls)

A live HTMLCollection

Live — automatically reflects later insertions/removals that match. Unlike NodeList, HTMLCollection has no .forEach; convert with Array.from(…​) first to use array methods.

let spinner = document.querySelector("#spinner");            // first match, or null
let titles = document.querySelectorAll("h1, h2, h3");        // every <h1>/<h2>/<h3>, as a NodeList
let sect1 = document.getElementById("sect1");                 // shortcut equivalent to querySelector("#sect1")
let tooltips = document.getElementsByClassName("tooltip");   // live NodeList/HTMLCollection

The crucial differences to keep straight:

  • Single element vs. collection — querySelector() and getElementById() each return one Element (or null if nothing matched); querySelectorAll() and the getElementsBy…​() family return a collection of every match.

  • Static vs. live — the NodeList returned by querySelectorAll() is a fixed snapshot: it never changes after the call, even if you later add or remove matching elements from the document. The collections returned by getElementsByTagName(), getElementsByClassName(), and getElementsByName() are live: their length and contents update automatically as the document changes. This distinction is a common source of subtle bugs when iterating over a live collection while modifying the document at the same time.

  • Both querySelector() and querySelectorAll() are also defined on Element, not just Document — calling someElement.querySelectorAll(…​) searches only among that element’s descendants.

A NodeList has a length property and can be indexed like an array, and it is iterable with for…​of, but it is not a real Array — pass it to Array.from() (see Arrays & Typed Arrays) to get access to map(), filter(), and the rest of the array methods:

let paragraphs = Array.from(document.querySelectorAll("p"));
let wordCounts = paragraphs.map(p => p.textContent.split(/\s+/).length);

Two related methods test a single element against a selector rather than searching the whole tree:

element.matches("h1,h2,h3,h4,h5,h6");   // true/false -- does this element match the selector?
event.target.closest("a[href]");        // nearest ancestor (or self) matching the selector, or null

closest() walks up the tree from an element looking for the nearest ancestor (including the element itself) that matches; it is the mirror image of querySelector(), which walks down. It is especially useful inside an event handler registered on a container element — see Events.

Document Structure and Traversal

Once an element has been selected, related elements are reachable through a small set of navigation properties defined on Element. These properties skip Text and Comment nodes and expose only the Element tree:

Property What it refers to

parentNode

The parent Element (or the Document, at the root).

children

A live NodeList of the Element children (excludes text/comment nodes).

childElementCount

The number of Element children — equivalent to children.length.

firstElementChild / lastElementChild

The first/last Element child, or null if none.

nextElementSibling / previousElementSibling

The neighboring Element immediately after/before this one, or null.

// Recursively visit e and every descendant Element, invoking f() on each
function traverse(e, f) {
  f(e);
  for (const child of e.children) {
    traverse(child, f);
  }
}

A parallel, lower-level set of properties is defined on every Node (not just Element), and includes Text and Comment nodes: parentNode, childNodes (all children, not just elements), firstChild/lastChild, nextSibling/previousSibling, plus nodeType (a numeric code — 1 for elements, 3 for text nodes, 8 for comments, 9 for the document) and nodeValue. This Node-level API is sensitive to incidental whitespace text nodes between tags, so day-to-day code almost always prefers the Element-only properties above.

Creating, Inserting, and Removing Elements

New elements are created with document.createElement(), which returns an empty, detached Element that must then be inserted somewhere into the tree to become visible:

let paragraph = document.createElement("p");   // an empty, detached <p>
let emphasis = document.createElement("em");
emphasis.append("World");                       // add a text node as a child
paragraph.append("Hello ", emphasis, "!");       // append() accepts strings and/or Nodes
paragraph.prepend("Hi ");                        // add content at the start instead of the end

append() and prepend() (defined on Element) each take any number of strings and/or Node arguments; string arguments are automatically wrapped in Text nodes. To insert content relative to an existing sibling rather than at the start/end of a parent, use before()/after() (defined on both Element and Text nodes):

let heading = document.querySelector("h2.greeting");
heading.after(paragraph);                                  // insert paragraph right after heading
heading.before(document.createElement("hr"));               // insert an <hr> right before heading

An element already in the document that gets inserted again is moved, not copied — use cloneNode(true) first if you want an independent copy (the true argument requests a deep clone, including descendants):

heading.after(paragraph.cloneNode(true));   // insert a copy, leaving the original in place

To remove an element (or replace it with something else), call its own remove() or replaceWith() method — no need to look up the parent first:

paragraph.remove();                 // detach paragraph from the document
heading.replaceWith(paragraph);     // replace heading with paragraph in one step

replaceChildren(…​nodesOrStrings) clears out all of an element’s existing children and replaces them with the given arguments in one call — a convenient alternative to first removing every child by hand and then appending new ones:

let list = document.querySelector("#items");
list.replaceChildren();                                  // remove every child -- an empty element
list.replaceChildren("first", document.createElement("li"));  // then replace with new content

An older generation of DOM methods — parent.appendChild(node), parent.insertBefore(node, refNode), parent.removeChild(node), and parent.replaceChild(newNode, oldNode) — accomplishes the same things but is more awkward to use: each one must be called on the parent node rather than on the node being inserted/removed, and unlike append()/prepend(), these older methods accept only Node arguments, never plain strings. They still appear constantly in existing code and library internals, so it is worth recognizing them even though append(), prepend(), before(), after(), remove(), and replaceWith() are easier to reach for in new code:

list.appendChild(document.createElement("li"));               // same effect as list.append(...)
list.insertBefore(newItem, list.children[1]);                  // insert newItem before the second child
list.removeChild(list.firstElementChild);                      // remove the first child
list.replaceChild(newItem, list.children[0]);                  // replace the first child with newItem

Attributes, Properties, and Element Content

HTML elements carry attributes — name/value pairs written in the markup, such as href on an <a> tag. The Element class defines general-purpose methods for working with any attribute by name:

let link = document.querySelector("a");
link.getAttribute("href");            // read the href attribute as a string
link.setAttribute("href", "/next");   // write it
link.hasAttribute("target");          // true/false
link.removeAttribute("target");       // remove it entirely

For the standard attributes of standard elements, it is usually more convenient to read and write the same information through a matching JavaScript property on the Element object — the browser keeps most of these in sync with the underlying attribute automatically:

let image = document.querySelector("#hero");
image.src;                 // mirrors the src attribute
image.id === "hero";       // true

let form = document.querySelector("form");
form.action = "/submit";   // sets the action attribute
form.method = "POST";

A few naming quirks to remember when mapping an HTML attribute name to its JavaScript property:

  • Multi-word attributes become camelCase properties (tabindextabIndex).

  • class is a reserved word in JavaScript, so the class attribute is exposed as the className property (a single space-separated string), or, more conveniently, as classList — a set-like object with add(), remove(), contains(), and toggle() methods for working with individual class names:

    spinner.classList.remove("hidden");
    spinner.classList.add("animated");
    spinner.classList.toggle("active");    // add it if absent, remove it if present
  • Custom data-* attributes are exposed through the dataset property — see below.

  • There is no property-based way to remove an attribute — delete element.someProp does not work for this; use removeAttribute().

data-* Attributes and the dataset Property

HTML permits any element to carry custom attributes named with a data- prefix (data-user-id, data-section-number, and so on) for attaching page-specific data that has no standard attribute of its own. These attributes are conventionally lowercase, hyphen-separated after the data- prefix, never affect how the browser renders or validates the element, and are otherwise ordinary attributes — readable and writable through getAttribute()/setAttribute() like any other.

The more convenient way to work with them is the element’s dataset property, which exposes every data-* attribute as a plain object of string values, with the data- prefix stripped and any remaining hyphens converted to camelCase: data-section-number becomes dataset.sectionNumber, data-user-id becomes dataset.userId. Reading or writing through dataset reads/writes the underlying attribute directly — there is no separate storage, so setting a dataset property is equivalent to calling setAttribute() with the hyphenated name.

// <li data-section-number="3" data-user-id="42">Item</li>
let item = document.querySelector("li");
item.dataset.sectionNumber;        // => "3" -- dataset values are always strings
item.dataset.userId;               // => "42"
item.dataset.sectionNumber = "4";  // updates the data-section-number attribute to "4"
"userId" in item.dataset;          // => true -- dataset supports the `in` operator like any object

Because dataset values are always strings, code that needs a number or boolean must convert explicitly (Number(item.dataset.sectionNumber)), the same as it would for any other attribute value read through getAttribute().

textContent vs. innerHTML

An element’s content can be read or written either as plain text or as an HTML string, and the two behave very differently:

Property Reads/writes Behavior

textContent

Plain text

Text is inserted/returned literally — no markup parsing, no markup escaping needed by the caller.

innerHTML

An HTML markup string

Setting it invokes the browser’s HTML parser and replaces the element’s children with the parsed result.

let para = document.querySelector("p");
para.textContent;               // the element's text, with all markup stripped
para.textContent = "Hello!";    // sets plain text -- any "<" or "&" is inserted literally, not parsed

para.innerHTML;                 // the element's content serialized back to an HTML string
para.innerHTML = "<b>Hi</b>";   // parses the string and replaces the element's children
Never pass unsanitized, user-controlled input into innerHTML (or outerHTML, or insertAdjacentHTML()). A string like <img src="x" onload="alert('hacked')"> is valid markup that the parser will happily execute — this is a cross-site scripting (XSS) vulnerability, one of the most common web security bugs. If the content to insert is plain text, use textContent instead, since it never parses its argument as markup. If it genuinely needs to be HTML, either build it from trusted, hardcoded strings only, or escape special characters (&, <, >, ", ') in any untrusted portion before it reaches innerHTML.

Because setting textContent never invokes the HTML parser, it is both safer and typically faster than innerHTML when the goal is simply to display text — reach for innerHTML only when the content is genuinely markup that needs to be parsed into real elements.

Where This Fits

Selecting, creating, and mutating elements is the foundation the rest of the browser platform builds on: user interaction is layered on top via Events, visual presentation via Scripting CSS, and on-screen position/size via Geometry & Scrolling. Elements created and inserted with the techniques above are also the targets of the animation, canvas, and media APIs covered later in this section.