HTML Interview Questions

20+ HTML interview questions and answers in quiz-style format, answered by ex-FAANG interviewers
Questions and solutions by ex-interviewers
Covers critical topics

HTML interview questions are designed to assess your understanding of web development fundamentals and best practices. Interviewers typically focus on key topics such as:

  • Accessibility: Ensuring websites are accessible to users with disabilities using semantic HTML and ARIA roles.
  • Semantics: Recognizing the importance of semantic HTML tags for SEO, accessibility, and code clarity.
  • Forms: Building forms with proper input validation, accessibility features, and efficient handling of form submissions.
  • Multimedia: Embedding and managing images, audio, and video in HTML while optimizing for performance and accessibility.
  • Best Practices: Structuring HTML for readability, maintainability, and performance, including the proper use of meta tags, link attributes, and media queries.
  • SEO Optimization: Using semantic HTML elements and metadata to boost search engine ranking and improve web performance.

Below, you’ll find 20+ carefully curated HTML interview questions covering everything from core concepts to best practices and optimization strategies.

Each question includes:

  • Quick Answers (TL;DR): Concise, clear responses to help you answer confidently.
  • Detailed Explanations: In-depth insights to ensure you not only know the answers but understand the reasoning behind them.

Unlike most lists, our questions are carefully curated by real senior and staff engineers from top tech companies like Amazon, Meta, and more, not anonymous contributors or AI-generated content. Start practicing below and get ready to ace your HTML interview!

If you're looking for HTML coding questions -We've got you covered as well, with:
Javascript coding
  • 70+ HTML coding interview questions
  • An in-browser coding workspace that mimics real interview conditions
  • Reference solutions from ex-interviewers at Big Tech companies
  • One-click automated, transparent test cases
  • Instant UI preview for UI-related questions
Get Started
Join 50,000+ engineers

Describe the difference between `<script>`, `<script async>` and `<script defer>`

Topics
HTMLJavaScript

TL;DR

All of these ways (<script>, <script async>, and <script defer>) are used to load and execute JavaScript files in an HTML document, but they differ in how the browser handles loading and execution of the script:

  • <script> is the default way of including JavaScript. The browser blocks HTML parsing while the script is being downloaded and executed. The browser will not continue rendering the page until the script has finished executing.
  • <script async> downloads the script asynchronously, in parallel with parsing the HTML. Executes the script as soon as it is available, potentially interrupting the HTML parsing. Multiple <script async> tags do not wait for each other and execute in no particular order.
  • <script defer> downloads the script asynchronously, in parallel with parsing the HTML. However, the execution of the script is deferred until HTML parsing is complete, in the order they appear in the HTML.

Here's a table summarizing the 4 ways of loading <script>s in an HTML document. Modern apps almost always use modules, which deserve their own row.

Feature<script><script async><script defer><script type="module">
Parsing behaviorBlocks HTML parsingDownloads in parallel; execution still blocks parsingDownloads in parallel; execution deferred until after parsingDownloads in parallel; execution deferred until after parsing
Execution orderIn order of appearanceNot guaranteedIn order of appearanceIn order of appearance, with each script's import dependencies resolved first
DOM state at executionOnly earlier markup is parsedDepends on download timingDocument parsing is completeDocument parsing is complete

Loading and execution timing

Both attributes allow fetching alongside HTML parsing, but async executes as soon as it is ready while defer waits for parsing and preserves document order.

async and defer script timing

An async script can execute before or after parsing completes and does not delay DOMContentLoaded once that event is otherwise ready.

What <script> tags are for

<script> tags are used to include JavaScript on a web page. The async and defer attributes are used to change how/when the loading and execution of the script happens.

<script>

For normal <script> tags without any async or defer, when they are encountered, HTML parsing is blocked, the script is fetched and executed immediately. HTML parsing resumes after the script is executed. This can block rendering of the page if the script is large.

Use <script> for critical scripts that the page relies on to render properly.

<!doctype html>
<html>
<head>
<title>Regular Script</title>
</head>
<body>
<!-- Content before the script -->
<h1>Regular Script Example</h1>
<p>This content will be rendered before the script executes.</p>
<!-- Regular script -->
<script src="regular.js"></script>
<!-- Content after the script -->
<p>This content will be rendered after the script executes.</p>
</body>
</html>

<script async>

In <script async>, the browser downloads the script file asynchronously (in parallel with HTML parsing) and executes it as soon as it is available (potentially before HTML parsing completes). The script will not necessarily be executed in the order in which it appears in the HTML document. This can improve perceived performance because the browser doesn't wait for the script to download before continuing to render the page.

Use <script async> when the script is independent of any other scripts on the page, for example, analytics and ads scripts.

<!doctype html>
<html>
<head>
<title>Async Script</title>
</head>
<body>
<!-- Content before the script -->
<h1>Async Script Example</h1>
<p>This content will be rendered before the async script executes.</p>
<!-- Async script -->
<script async src="async.js"></script>
<!-- Content after the script -->
<p>
This content may be rendered before or after the async script executes.
</p>
</body>
</html>

<script defer>

Similar to <script async>, <script defer> downloads in parallel with HTML parsing, but executes after parsing and before DOMContentLoaded. Deferred classic scripts preserve document order. They also wait for stylesheets that block scripts, which can indirectly delay DOMContentLoaded.

If a script relies on a fully-parsed DOM, the defer attribute will be useful in ensuring that the HTML is fully parsed before executing.

<!doctype html>
<html>
<head>
<title>Deferred Script</title>
</head>
<body>
<!-- Content before the script -->
<h1>Deferred Script Example</h1>
<p>This content will be rendered before the deferred script executes.</p>
<!-- Deferred script -->
<script defer src="deferred.js"></script>
<!-- Content after the script -->
<p>This content will be rendered before the deferred script executes.</p>
</body>
</html>

<script type="module">

Module scripts are the standard entry point for projects built with Vite (and many other modern bundlers). Next.js doesn't always emit type="module" for its own runtime, but most application code authored as ES modules ends up running through one of these tags. They behave like defer scripts with two important additions: dependencies declared with import are loaded and executed in the right order, and module code is strict by default.

<script type="module" src="/src/main.js"></script>

Behavior:

  • Parsing: deferred. HTML parsing continues; the script does not block.
  • Execution: runs after the document has finished parsing. Across multiple <script type="module"> tags in the same document, execution follows document order, but each script's import dependencies are resolved and executed first.
  • DOM ready: the DOM is parsed before the module runs.
  • Strict mode: enforced automatically; no 'use strict' directive needed.
  • CORS: module scripts are always fetched with CORS, so the server must send Access-Control-Allow-Origin for cross-origin loads. The crossorigin attribute itself is not required for the module to load — it only controls whether credentials (cookies, HTTP auth) are sent on cross-origin requests (crossorigin or crossorigin="anonymous" omits them; crossorigin="use-credentials" sends them). Adding it is still recommended for explicit credentials handling and for full error details in error event handlers.

Use <script type="module"> for new front-end code. If you have an independent module-script entry point and document order does not matter, combine with async:

<script async type="module" src="/analytics-module.js"></script>

Which to use: a decision matrix

Script typeUse for
<script> (no attrs)Critical inline scripts that must run synchronously before the next HTML element parses.
<script async>Independent third-party scripts where order does not matter (analytics, ads, monitoring beacons).
<script defer>Classic (non-module) app scripts where order matters and the DOM should be ready.
<script type="module">Native ES module entry points and bundler output that preserves modules.
<script async type="module">Module scripts where order does not matter. Most apps want default module behavior instead.

How build tools load scripts

It also helps to know what the tools you use actually generate.

  • Build tools may emit module scripts, classic deferred scripts, preload hints, or injected runtime loaders. Inspect the generated HTML instead of assuming a framework maps directly to one native attribute.
  • Independent third-party scripts are often candidates for async; use defer when document order or DOMContentLoaded timing matters. Follow a vendor's integration constraints and measure the effect either way.

Common bugs from the wrong attribute choice

  • Independent script loaded with defer unnecessarily. A deferred script runs before DOMContentLoaded and preserves order with other deferred scripts. If neither property is needed, async may avoid delaying DOMContentLoaded; verify that the script has no ordering dependency first.
  • App entry as async script. If app.js and vendor.js are loaded with async, they can execute in any order. app.js may run before vendor.js finishes, which throws ReferenceError for the missing globals. Use defer (or modules) for app scripts.
  • document.write inside a defer or async script. Browsers ignore document.write() calls from async or deferred scripts with a console warning: "A call to document.write() from an asynchronously-loaded external script was ignored." The script would need to be a regular blocking <script> for the call to take effect, though document.write should be avoided in any new code.
  • Module script in a <script> tag without type="module". Top-level import statements throw SyntaxError. Either set type="module" or use a bundler.
  • Cross-origin module served without CORS headers. Module scripts are always fetched with CORS, so a cross-origin URL like <script type="module" src="https://cdn.example.com/lib.js"> will fail to load if the server doesn't send Access-Control-Allow-Origin. The fix is on the server side — adding crossorigin to the tag does not bypass this requirement (though it is still recommended for better error reporting and explicit credentials handling).

Notes

  • The async attribute should be used for scripts that are not critical to the initial rendering of the page and do not depend on each other, while the defer attribute should be used for scripts that depend on or are depended on by another script.
  • defer has no effect on inline scripts, and async has no effect on inline classic scripts. async can affect an inline module script because its dependency graph still has to be fetched.
  • <script>s with defer or async that contain document.write() will be ignored with a message like "A call to document.write() from an asynchronously-loaded external script was ignored".
  • Even though async and defer help to make script downloading asynchronous, the scripts are still eventually executed on the main thread. If these scripts are computationally intensive, it can result in laggy/frozen UI. Partytown is a library that helps relocate script executions into a web worker and off the main thread, which is great for third-party scripts where you do not have control over the code.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which statements about classic external scripts are correct? Select all that apply.

What is the difference between `mouseenter` and `mouseover` event in JavaScript and browsers?

Topics
Web APIsHTMLJavaScript

TL;DR

The main difference lies in the bubbling behavior of mouseenter and mouseover events. mouseenter does not bubble while mouseover bubbles.

mouseenter events do not bubble. The mouseenter event is triggered only when the mouse pointer enters the element itself, not its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseenter event will not be triggered on the parent element again; it is only triggered once upon entry of the parent element, without regard for its contents. If both parent and child have mouseenter listeners attached and the mouse pointer moves from the parent element to the child element, mouseenter will only fire for the child.

mouseover events bubble up the DOM tree. The mouseover event is triggered when the mouse pointer enters the element or one of its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseover event will be triggered on the parent element again as well. If the parent element has multiple child elements, this can result in multiple event callbacks fired. If there are child elements, and the mouse pointer moves from the parent element to the child element, mouseover will fire for both the parent and the child.

Propertymouseentermouseover
BubblingNoYes
TriggerOnly when entering itselfWhen entering itself and when entering descendants

Moving from a parent into its child

The behavioral difference is clearest when the pointer crosses an internal descendant boundary.

mouseenter and mouseover across nested elements

mouseenter models entry into an element as a whole and does not bubble, while mouseover fires for descendant crossings and bubbles.

mouseenter event:

  • Does not bubble: The mouseenter event does not bubble. It is only triggered when the mouse pointer enters the element to which the event listener is attached, not when it enters any child elements.
  • Triggered once: The mouseenter event is triggered only once when the mouse pointer enters the element, making it more predictable and easier to manage in certain scenarios.

A use case for mouseenter is when you want to detect the mouse entering an element without worrying about child elements triggering the event multiple times.

mouseover event:

  • Bubbles up the DOM: The mouseover event bubbles up through the DOM. This means that if you have an event listener on a parent element, it will also trigger when the mouse pointer moves over any child elements.
  • Triggered multiple times: The mouseover event is triggered every time the mouse pointer moves over an element or any of its child elements. This can lead to multiple triggers if you have nested elements.

A use case for mouseover is when you want to detect when the mouse enters an element or any of its children and are okay with the events triggering multiple times.

Example

Here's an example demonstrating the difference between mouseover and mouseenter events:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mouse Events Example</title>
<style>
.parent {
width: 200px;
height: 200px;
background-color: lightblue;
padding: 20px;
}
.child {
width: 100px;
height: 100px;
background-color: lightcoral;
}
</style>
</head>
<body>
<div class="parent">
Parent Element
<div class="child">Child Element</div>
</div>
<script>
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');
// Mouseover event on parent.
parent.addEventListener('mouseover', () => {
console.log('Mouseover on parent');
});
// Mouseenter event on parent.
parent.addEventListener('mouseenter', () => {
console.log('Mouseenter on parent');
});
// Mouseover event on child.
child.addEventListener('mouseover', () => {
console.log('Mouseover on child');
});
// Mouseenter event on child.
child.addEventListener('mouseenter', () => {
console.log('Mouseenter on child');
});
</script>
</body>
</html>

Expected behavior

  • When the mouse enters the parent element:
    • The mouseover event on the parent will trigger.
    • The mouseenter event on the parent will trigger.
  • When the mouse enters the child element:
    • The mouseover event on the parent will trigger again because mouseover bubbles up from the child.
    • The mouseover event on the child will trigger.
    • The mouseenter event on the child will trigger.
    • The mouseenter event on the parent will not trigger again because mouseenter does not bubble.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A list must observe pointer entry involving rows added after listener setup, using one bubbling listener on the list. Which registration strategy fits?

Explain the difference between `document.querySelector()` and `document.getElementById()`

Topics
Web APIsJavaScriptHTML

TL;DR

document.querySelector() and document.getElementById() are both methods used to select elements from the DOM, but they have key differences. document.querySelector() can select any element using a CSS selector and returns the first match, while document.getElementById() selects an element by its ID and returns the element with that specific ID.

// Using document.querySelector()
const element = document.querySelector('.my-class');
// Using document.getElementById()
const elementById = document.getElementById('my-id');

Difference between document.querySelector() and document.getElementById()

document.querySelector()

  • Can select elements using any valid CSS selector, including class, ID, tag, attribute, and pseudo-classes
  • Returns the first element that matches the specified selector
  • More versatile but slightly slower due to the flexibility of CSS selectors
// Select the first element with the class 'my-class'
const element = document.querySelector('.my-class');
// Select the first <div> element
const divElement = document.querySelector('div');
// Select the first element with the attribute data-role='button'
const buttonElement = document.querySelector('[data-role="button"]');

document.getElementById()

  • Selects an element by its ID attribute
  • Returns the element with the specified ID
  • Faster and more efficient for selecting elements by ID, but less versatile
// Select the element with the ID 'my-id'
const elementById = document.getElementById('my-id');

Key differences

  • Selector type: document.querySelector() uses CSS selectors, while document.getElementById() uses only the ID attribute.
  • Return value: document.querySelector() returns the first matching element, whereas document.getElementById() returns the element with the specified ID.
  • Performance: document.getElementById() is generally faster because it directly accesses the element by ID, while document.querySelector() has to parse the CSS selector.

The full DOM-query method comparison

querySelector and getElementById are two of the seven main DOM-query methods. The most important practical distinction across all of them is whether the result is a live or a static collection.

MethodSelector inputReturnsLive?When to use
getElementById(id)id stringElement or nullNo (single element)Single element by id; hot-path code
querySelector(sel)Any CSS selectorFirst match or nullNo (snapshot)First element matching any selector
querySelectorAll(sel)Any CSS selectorStatic NodeListNo (snapshot)All matches as a frozen list
getElementsByClassName(name)Class nameHTMLCollectionYes (auto-updates)When you need a live collection
getElementsByTagName(tag)Tag nameHTMLCollectionYesSame
getElementsByName(name)name attributeNodeListYesForm elements by name
closest(sel)Any CSS selectorNearest matching ancestor (or self) or nullN/AWalking up from a target inside event handlers

The live vs snapshot distinction is a common source of subtle bugs.

Live vs static collections (predict the output)

document.body.innerHTML = '<div class="x"></div><div class="x"></div>';
const live = document.getElementsByClassName('x'); // HTMLCollection (live)
const snapshot = document.querySelectorAll('.x'); // NodeList (static)
console.log('before:', live.length, snapshot.length); // 2, 2
document.body.insertAdjacentHTML('beforeend', '<div class="x"></div>');
console.log('after: ', live.length, snapshot.length); // 3, 2

The live HTMLCollection updates automatically when DOM nodes are added or removed. The static NodeList from querySelectorAll does not.

This matters in practice in two specific ways:

  • Iterating a live collection while mutating it is a classic infinite-loop bug. Reading live[i] after appending more matching nodes hits the new ones too, and the for loop never finishes.
  • A cached querySelectorAll result is a stable snapshot; a cached getElementsByClassName result changes as the DOM changes. Neither is universally better: the snapshot can become stale, while the live collection can change during iteration.

Prefer querySelectorAll for most modern use cases (it has forEach, and array spread [...] works on it). Reach for the live collections only when you specifically want auto-updating behavior.

Performance: how much does it actually matter?

getElementById() gives the browser a narrower lookup than a general CSS selector, but engine strategies and timings vary with the document and browser. In ordinary application code, choose the API that expresses the lookup clearly. If a real performance trace identifies repeated DOM queries in a hot path, reduce the number of queries or cache a valid reference and measure again.

Complex selectors may require more work than an ID lookup, but invented per-call timings and isolated microbenchmarks do not establish user impact. DOM mutation, style, layout, rendering, and the work performed after selection are often more significant.

If you genuinely need maximum speed for repeated lookups, cache the reference:

const button = document.getElementById('action');
button.addEventListener('click', handle);

Keep in mind that a cached element can become detached or be replaced. A long-lived reference is correct only while that element remains the intended target.

What about closest(), matches(), and contains()?

Three more methods round out the modern DOM-query toolkit:

  • element.closest(selector) walks up from the element (including the element itself) and returns the nearest ancestor matching the selector, or null. It is constantly useful in event-delegation handlers:

    table.addEventListener('click', (event) => {
    const row = event.target.closest('tr[data-id]');
    if (row) editRow(row.dataset.id);
    });
  • element.matches(selector) returns true or false for whether the element matches the CSS selector. Useful inside delegated handlers when you want to confirm the target type without a tagName check.

  • parent.contains(child) returns true if child is parent or anywhere inside it. Useful for outside-click detection: if (!modal.contains(event.target)) close().

These three plus querySelector and querySelectorAll form the modern toolkit. The older getElementsBy* methods are legacy and rarely the right default in new code.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

Code needs the first enabled submit button inside #checkout, expressed as the selector #checkout button[type="submit"]:not(:disabled). Which API directly accepts that query?

How do `<iframe>` on a page communicate?

Topics
Web APIsJavaScriptHTML

TL;DR

Parent pages and iframes can communicate across origins with postMessage(). It is secure only when the sender uses the exact target origin and the receiver validates event.origin, usually event.source, and the shape of event.data.

// In the parent page
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello from parent', 'https://widget.example');
// In the iframe
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent.example') return;
if (event.source !== window.parent) return;
console.log(event.data); // 'Hello from parent'
});

Choose communication by origin

Direct DOM access is available only when both documents satisfy the same-origin policy; postMessage() is the explicit cross-origin channel.

iframe communication by origin

Always use a specific targetOrigin when sending sensitive data and verify both event.origin and, when possible, event.source when receiving.

How do <iframe> on a page communicate?

Using the postMessage API

The postMessage API is the most common and secure way for iframes to communicate with each other or with their parent page. This method allows for cross-origin communication, which is essential for modern web applications.

Sending a message

To send a message from the parent page to the iframe, you can use the postMessage method. Here’s an example:

// In the parent page
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello from parent', 'https://widget.example');

The second argument is the origin the receiving window must have for the message to be delivered. Avoid '*' when the destination has a known origin, because the frame can navigate to an unexpected origin between obtaining the window reference and sending the message.

Receiving a message

To receive a message in the iframe, you need to add an event listener for the message event:

// In the iframe
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent.example') return;
if (event.source !== window.parent) return;
console.log(event.data); // 'Hello from parent'
});

The event object contains the data property, which holds the message sent by the parent page.

Security considerations

When using postMessage, it's crucial to consider security:

  • Specify the target origin: Instead of using '*', specify the exact origin expected for the receiving window.
  • Validate the sender: Check event.origin and, where possible, event.source. targetOrigin protects the receiver; it does not authenticate messages arriving at your listener.
  • Validate the message: Treat event.data as untrusted input and validate its type and fields before using it.

Example with target origin

Here’s an example with a specified target origin:

// In the parent page
const iframe = document.querySelector('iframe');
const targetOrigin = 'https://example.com';
iframe.contentWindow.postMessage('Hello from parent', targetOrigin);
// In the iframe
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent.com') return;
if (event.source !== window.parent) return;
if (typeof event.data !== 'string') return;
console.log(event.data); // 'Hello from parent'
});

In this example, the parent page sends a message only to https://example.com, and the iframe processes the message only if it comes from https://parent.com.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A parent embeds two frames from https://widgets.example: a payment frame and a help frame. Both can send { type: "payment-complete" }, but only the payment frame is authorized to complete checkout. The receiver already checks the exact origin and validates this message shape. What additional check distinguishes the authorized sender?

How do you add, remove, and modify HTML elements using JavaScript?

Topics
Web APIsJavaScriptHTML

TL;DR

Create elements with document.createElement(), set text with textContent, and insert them with append(), prepend(), before(), after(), or replaceWith(). Remove an element with remove(). Use classList, properties, and attributes for targeted updates. Avoid assigning untrusted strings to innerHTML; use text and DOM construction, or an appropriate HTML sanitizer when the product intentionally accepts HTML.

// Adding an element
const newElement = document.createElement('div');
newElement.textContent = 'Hello, World!';
document.body.appendChild(newElement);
// Removing an element
const elementToRemove = document.getElementById('elementId');
elementToRemove?.remove();
// Modifying an element
const elementToModify = document.getElementById('elementId');
if (elementToModify) elementToModify.textContent = 'New content';

Adding, removing, and modifying HTML elements using JavaScript

Adding elements

To add an HTML element, you can use the document.createElement method to create a new element and then append it to a parent element using appendChild.

// Create a new div element
const newDiv = document.createElement('div');
// Set its content
newDiv.textContent = 'Hello, World!';
// Append the new element to the body
document.body.appendChild(newDiv);
// See the changed document by running the code
console.log(document.body);

You can also use insertBefore to insert the new element before an existing child element.

const parentElement = document.getElementById('parent');
const newElement = document.createElement('p');
newElement.textContent = 'Inserted Paragraph';
const referenceElement = document.getElementById('reference');
parentElement.insertBefore(newElement, referenceElement);

Removing elements

To remove an HTML element, you can use the removeChild method on its parent element. Check that both references exist and the node is still that parent's child when the DOM may change asynchronously.

// Select the element to be removed
const elementToRemove = document.getElementById('elementId');
// Remove the element
elementToRemove.parentNode.removeChild(elementToRemove);

Alternatively, you can use the remove method directly on the element.

const elementToRemove = document.getElementById('elementId');
elementToRemove.remove();

Modifying elements

To modify an HTML element, you can change its properties such as innerHTML, textContent, or attributes.

const elementToModify = document.createElement('div');
// Change its text content
elementToModify.textContent = 'New Text Content';
// Change an attribute
elementToModify.setAttribute('class', 'new-class');
console.log(elementToModify);

When intentional, trusted markup is already available as DOM nodes, replaceChildren() can replace a container without string parsing:

const message = document.createElement('strong');
message.textContent = 'Saved';
elementToModify.replaceChildren(message);

You can also use methods like classList.add, classList.remove, and classList.toggle to modify the element's classes.

const element = document.getElementById('elementId');
// Add a class
element.classList.add('new-class');
// Remove a class
element.classList.remove('old-class');
// Toggle a class
element.classList.toggle('active');

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which DOM updates are appropriate for a list whose labels come from untrusted user input? Select all that apply.

What is the difference between `event.preventDefault()` and `event.stopPropagation()`?

Topics
Web APIsHTMLJavaScript

TL;DR

event.preventDefault() cancels an event's default browser action when the event is cancelable, such as link navigation or form submission. event.stopPropagation() stops the event from continuing through the remaining capture and bubble path. It does not cancel the default action or stop other listeners on the same element; use stopImmediatePropagation() for the latter.


Two independent effects

Event dispatch and the browser's default action are separate concerns, so stopping one does not automatically stop the other.

Event propagation and default action controls

stopImmediatePropagation() additionally blocks later listeners on the current node; preventDefault() has an effect only when the event is cancelable.

What is the difference between event.preventDefault() and event.stopPropagation()?

event.preventDefault()

event.preventDefault() is a method that cancels the event if it is cancelable, meaning that the default action that belongs to the event will not occur. For example, this can be used to prevent a form from being submitted:

document.querySelector('form').addEventListener('submit', function (event) {
event.preventDefault();
// Form submission is prevented
});

event.stopPropagation()

event.stopPropagation() prevents the event from continuing through the DOM propagation path. In a bubble listener it prevents later ancestor listeners from seeing the event; in a capture listener it can prevent the event from reaching the target at all. It does not stop other listeners already registered on the current element.

document.querySelector('.child').addEventListener('click', function (event) {
event.stopPropagation();
// Click event will not propagate to parent elements
});

Key differences

  • event.preventDefault() stops the default action associated with the event.
  • event.stopPropagation() stops further capture or bubbling through the event path.
  • event.stopImmediatePropagation() additionally stops later listeners on the current element.

Use cases

  • Use event.preventDefault() when you want to prevent the default behavior of an element, such as preventing a link from navigating or a form from submitting.
  • Use event.stopPropagation() when you want to prevent an event from reaching parent elements, which can be useful in complex UIs where multiple elements have event listeners.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which statements about event-control methods are correct? Select all that apply.

What is the difference between `innerHTML` and `textContent`?

Topics
Web APIsHTMLJavaScript

TL;DR

innerHTML gets or replaces serialized HTML markup, so assigning to it invokes the HTML parser and creates elements. textContent gets or replaces text and treats < and > as characters. Use textContent for untrusted plain text. Use innerHTML only when HTML is intentionally required and the value is trusted or processed by an appropriate HTML sanitizer; assigning arbitrary user input creates an XSS sink.

// Example of innerHTML
element.innerHTML = '<strong>Bold Text</strong>'; // Renders as bold text
// Example of textContent
element.textContent = '<strong>Bold Text</strong>'; // Renders as plain text: <strong>Bold Text</strong>

Text insertion versus HTML parsing

The APIs send the same string through different browser pipelines.

innerHTML and textContent processing

Use textContent for plain text. Use innerHTML only when HTML interpretation is intentional and the value comes from a trusted or correctly sanitized source.

Difference between innerHTML and textContent

innerHTML

innerHTML is a property that allows you to get or set the HTML markup contained within an element. It can parse and render HTML tags, making it useful for dynamically updating the structure of a webpage.

Example
const element = document.getElementById('example');
element.innerHTML = '<strong>Bold Text</strong>'; // This will render as bold text
Use cases
  • Dynamically adding or updating HTML content
  • Rendering HTML tags and elements
Security considerations

Using innerHTML can expose your application to Cross-Site Scripting (XSS) attacks if you insert untrusted content. Use textContent when the value is plain text. If users are intentionally allowed to author a limited HTML subset, process it with a maintained, allowlist-based HTML sanitizer before insertion and consider enforcing Trusted Types.

textContent

textContent is a property that allows you to get or set the text content of an element. It ignores any HTML tags and renders them as plain text, making it safer for inserting user-generated content.

Example
const element = document.getElementById('example');
element.textContent = '<strong>Bold Text</strong>'; // This will render as plain text: <strong>Bold Text</strong>
Use cases
  • Safely inserting user-generated content
  • Inserting a string as literal text without parsing markup
Performance considerations

textContent avoids HTML parsing, but performance depends on the operation and document. Choose between the APIs for semantics and security first; profile a real bottleneck before making a performance claim.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

A comment value is untrusted plain text and may contain <img src=x onerror=...>. Which assignment safely displays those characters without interpreting them as markup?

What is the DOM and how is it structured?

Topics
JavaScriptHTML

TL;DR

The DOM, or Document Object Model, is a programming interface for web documents. It represents the page so that programs can change the document structure, style, and content. The DOM is structured as a tree of objects, where each node represents part of the document, such as elements, attributes, and text.


A document as a node tree

The DOM represents a parsed document as connected node objects; elements, text, and comments are node types within that tree.

Simplified DOM tree

DOM APIs navigate and mutate these node relationships; the tree is an in-memory object model, not the original HTML source text.

What is the DOM and how is it structured?

Definition

The Document Object Model (DOM) is a cross-platform and language-independent interface that treats an HTML, XHTML, or XML document as a tree structure. Each node in this tree represents a part of the document.

Structure

The DOM is structured as a hierarchical tree of nodes. Here are the main types of nodes:

  1. Document node: The root of the document tree. It represents the entire document.
  2. Element nodes: These represent HTML elements and form the bulk of the document tree.
  3. Attribute nodes: These are associated with element nodes and represent the attributes of those elements.
  4. Text nodes: These represent the text content within elements.
  5. Comment nodes: These represent comments in the HTML.

Example

Consider the following HTML:

<!doctype html>
<html>
<head>
<title>Document</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a paragraph.</p>
</body>
</html>

The DOM tree for this document would look like this:

Document
└── html
├── head
│ └── title
│ └── "Document"
└── body
├── h1
│ └── "Hello, World!"
└── p
└── "This is a paragraph."

Accessing and manipulating the DOM

JavaScript can be used to access and manipulate the DOM. Here are some common methods:

  • document.getElementById(id): Selects an element by its ID.
  • document.querySelector(selector): Selects the first element that matches a CSS selector.
  • element.appendChild(node): Adds a new child node to an element.
  • element.removeChild(node): Removes a child node from an element.

Example:

// Create an <h1> element and add it to the DOM
const newElement = document.createElement('h1');
document.body.appendChild(newElement);
// Get the h1 element using querySelector
const heading = document.querySelector('h1');
heading.textContent = 'Hello, DOM!';
console.log(heading); // <h1>Hello, DOM!</h1>

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Consider exactly <p id="greeting">Hi <b>Ada</b>!</p>, with no extra whitespace. Which description of the p element’s direct children is correct?

What's the difference between an "attribute" and a "property" in the DOM?

Topics
Web APIsJavaScriptHTML

TL;DR

Attributes are defined in the HTML and provide initial values for properties. Properties are part of the DOM and represent the current state of an element. For example, the value attribute of an <input> element sets its initial value, while the value property reflects the current value as the user interacts with it.


Markup state and live object state

Attributes belong to serialized markup, while properties belong to the live DOM object. Some named pairs reflect changes, but reflection is defined per property.

HTML attributes and DOM properties

For example, an input's value attribute can remain the default value while its value property tracks current user-edited state.

Difference between an "attribute" and a "property" in the DOM

Attributes

Attributes are defined in the HTML markup and provide initial values for elements. They are static and do not change once the page is loaded unless explicitly modified using JavaScript.

Example
<input type="text" value="initial value" />

In this example, value="initial value" is an attribute.

Properties

Properties are part of the DOM and represent the current state of an element. They are dynamic and can change as the user interacts with the page or through JavaScript.

Example
const inputElement = document.querySelector('input');
console.log(inputElement.value); // Logs the current value of the input element
inputElement.value = 'new value'; // Changes the current value of the input element

In this example, value is a property of the inputElement object.

Key differences

  • Initialization: Attributes initialize DOM properties.
  • State: Attributes are static, while properties are dynamic.
  • Access: Attributes can be accessed using getAttribute and setAttribute methods, while properties can be accessed directly on the DOM object.
Example
<input id="myInput" type="text" value="initial value" />
const inputElement = document.getElementById('myInput');
// Accessing attribute
console.log(inputElement.getAttribute('value')); // "initial value"
// Accessing property
console.log(inputElement.value); // "initial value"
// Changing property
inputElement.value = 'new value';
console.log(inputElement.value); // "new value"
console.log(inputElement.getAttribute('value')); // "initial value"

In this example, changing the value property does not affect the value attribute.

The classic example: input value (try it live)

This is the canonical interview demonstration. Open the input below, type into it, and watch how the attribute stays at the initial value while the property tracks what the user typed:

document.body.innerHTML = `
<input id="demo" type="text" value="initial value" />
<button id="check">Log attribute vs property</button>
<pre id="out"></pre>
`;
const input = document.getElementById('demo');
const out = document.getElementById('out');
document.getElementById('check').addEventListener('click', () => {
out.textContent =
`attribute (getAttribute('value')): ${input.getAttribute('value')}\n` +
`property (input.value): ${input.value}`;
});
// Programmatic demo if no user typing happens:
input.value = 'typed by user';
out.textContent =
`attribute (getAttribute('value')): ${input.getAttribute('value')}\n` +
`property (input.value): ${input.value}`;

The takeaway: the attribute is the original markup; the property is the current state. They diverge the moment the user (or your code) interacts with the element.

To reset an input back to its attribute, set the property back to the attribute value (input.value = input.defaultValue). To persist a new initial value across resets, you have to set the attribute too (input.setAttribute('value', '...') or input.defaultValue = '...').

Other attributes that diverge from their properties

The value attribute is the most-cited example, but several other attributes have a meaningfully different DOM property. Each of these comes up regularly in real bugs.

checked on checkboxes and radios

document.body.innerHTML = `<input type="checkbox" id="cb" checked />`;
const cb = document.getElementById('cb');
console.log(cb.getAttribute('checked')); // "" (present in markup)
console.log(cb.checked); // true
cb.checked = false; // user un-checks (or your code does)
console.log(cb.getAttribute('checked')); // still "" (attribute did not change)
console.log(cb.checked); // false

Same pattern: the attribute is the default state used on form reset; the property is the current state.

disabled on inputs and buttons

document.body.innerHTML = `<button id="b">Submit</button>`;
const b = document.getElementById('b');
b.setAttribute('disabled', 'disabled'); // sets the attribute
console.log(b.disabled); // true (property reflects the attribute)
b.disabled = false; // sets the property
console.log(b.getAttribute('disabled')); // null (attribute is removed)

Boolean attributes like disabled, readonly, required, and hidden are reflected: setting the property automatically adds or removes the attribute. This is different from value/checked, where the attribute stays put.

class (attribute) vs className and classList (properties)

document.body.innerHTML = `<div id="d" class="a b c"></div>`;
const d = document.getElementById('d');
console.log(d.getAttribute('class')); // "a b c"
console.log(d.className); // "a b c" (same string)
console.log(d.classList); // DOMTokenList ['a', 'b', 'c']
d.classList.add('d');
console.log(d.getAttribute('class')); // "a b c d"

The attribute is class; the DOM property is className (because class is a reserved word in JavaScript). The modern classList API gives you add/remove/toggle methods that update both for you.

for (attribute) vs htmlFor (property)

The for attribute on a <label> becomes the htmlFor property, for the same reason (for is a reserved word in JavaScript). This is a common stumbling block for React developers because JSX uses htmlFor:

// React JSX uses the property name
<label htmlFor="email">Email</label>;
// Plain HTML uses the attribute name
<label for="email">Email</label>;

style: string vs object

The style attribute is a string. The style property is a CSSStyleDeclaration object:

element.getAttribute('style'); // 'color: red; font-size: 14px' (string)
element.style; // CSSStyleDeclaration object; element.style.color === 'red'

You read individual rules from the property (element.style.fontSize) but cannot read computed values that way. For computed styles, use getComputedStyle(element).fontSize.

Why this matters in React, Vue, and other frameworks

Frameworks set properties in some places and attributes in others, and knowing which they choose explains a class of "this attribute won't update" bugs.

  • React sets DOM properties when a matching one exists, so <input value={x}> updates the property, not the attribute. This is why defaultValue exists: it is the way to set the underlying attribute. The same pattern applies to defaultChecked on checkboxes.
  • Vue 3 intelligently chooses between property and attribute for each binding. For standard interactive elements (such as value on <input>, <select>, and <progress>), it sets the DOM property. You can force one or the other with the .prop and .attr modifiers on v-bind.
  • Custom elements declared with attributes need attributeChangedCallback and observedAttributes to react to attribute changes. Properties do not go through that path.

If you ever see a React form where setting <input value={state}> works fine, but inspecting the rendered DOM shows the value="..." attribute stuck at the old value, that is not a bug. The attribute is intentionally the initial-value marker; the property is what reflects the live state.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 4
Check your understanding Exercise 1 of 4

Given <input id="name" value="initial">, what does this code log after it runs?

const input = document.querySelector('#name');
input.value = 'edited';
console.log(input.value, input.getAttribute('value'));

Why is it generally a good idea to position CSS `<link>`s between `<head></head>` and JS `<script>`s just before `</body>`?

Do you know any exceptions?
Topics
HTMLPerformance

TL;DR

Put render-blocking stylesheets in <head> so the browser discovers them early and can render styled content without a flash of unstyled content. The old advice to put every script just before </body> avoids parser blocking, but modern pages usually put application scripts in <head> with defer or type="module"; they download while HTML is parsed and execute after parsing. Use async only for independent scripts whose execution order does not matter.

<head>
<link rel="stylesheet" href="/styles.css" />
<script src="/app.js" defer></script>
<script type="module" src="/features.js"></script>
</head>

Why is it generally a good idea to position CSS <link>s between <head></head> and JS <script>s just before </body>?

The placement controls when the browser discovers a resource and whether fetching or executing it delays HTML parsing or rendering.

Put required stylesheets in <head>

The browser can parse HTML and fetch external CSS concurrently, but it normally waits for required stylesheets before painting content that depends on them. Discovering CSS in <head> starts that request early and reduces the chance of rendering unstyled content and repainting it later.

CSS that is not needed for the initial view can be loaded conditionally, split by route, or guarded by an appropriate media attribute. Inline only genuinely critical CSS: inlining too much prevents caching and makes the HTML larger.

Choose the script behavior explicitly

The important distinction is not simply “head versus body”; it is the script's loading mode.

ScriptFetching and execution behaviorAppropriate use
Classic <script src>Fetches and executes immediately, blocking the HTML parserRare scripts that must run at that exact parser position
<script defer>Fetches in parallel, executes in document order after parsing, before DOMContentLoadedMost classic application scripts
<script type="module">Fetches the module graph and defers execution by defaultModern module-based applications
<script async>Fetches in parallel and executes as soon as ready; order is not guaranteedIndependent analytics or advertising scripts
Script before </body>Is discovered late but does not block most document parsingLegacy pages that cannot use defer

A deferred script can safely query elements parsed from the document without waiting for load. The load event waits for additional resources such as images and is usually later than necessary.

Exceptions and tradeoffs

  • A tiny inline bootstrap may need to run early, for example to set a theme class before the first paint. Keep it small and compatible with the site's Content Security Policy.
  • A third-party script that is independent of application code can use async, but its cost should still be measured.
  • A script whose output must appear at a particular parser position is inherently parser-blocking. Prefer DOM APIs and templates over document.write().
  • Omitting optional <head> or <body> tags does not create a meaningful performance optimization and usually makes source markup harder to inspect.

Use the Network and Performance panels to verify discovery time, parser blocking, render-blocking resources, and actual paint timings rather than assuming one placement is fastest for every page.

Further reading

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which resource-loading choices are sound for a modern document? Select all that apply.

Consider HTML5 as an open web platform. What are the building blocks of HTML5?

Topics
BrowserHTML

TL;DR

“HTML5 as an open web platform” is a historical umbrella for semantic HTML plus related browser technologies: CSS styling, audio and video, graphics, networking, storage and offline support, performance, and device integration. These features are now maintained in separate living standards, so HTML5 should not be treated as one versioned API bundle.


Consider HTML5 as an open web platform. What are the building blocks of HTML5?

The phrase became popular when the modern web platform was being introduced. An interview-ready answer can name the traditional categories while clarifying that many of their APIs are not defined by the HTML specification itself.

Traditional building blocks

  • Semantics: Elements such as <main>, <nav>, <article>, and native form controls describe document structure and behavior.
  • Styling: CSS controls presentation, responsive layout, animation, and visual effects.
  • Multimedia: <audio>, <video>, and timed text provide native media playback.
  • Graphics: Canvas, SVG, and WebGL support raster, vector, and 2D or 3D graphics.
  • Connectivity: Fetch, server-sent events, and WebSocket connect pages to servers.
  • Storage and offline behavior: Web Storage, IndexedDB, Cache Storage, and service workers store data and support offline experiences. The old Application Cache feature is obsolete.
  • Performance and integration: Workers, performance APIs, and asynchronous loading help applications stay responsive and measurable.
  • Device access: Platform APIs can expose capabilities such as media capture, geolocation, sensors, and input devices, generally subject to permissions and secure-context rules.

How to discuss it today

HTML is now a Living Standard rather than a sequence of marketing versions. Browser APIs evolve independently and differ in compatibility, permissions, privacy impact, and failure modes. For a real feature, check the API's current specification and browser support instead of assuming “HTML5 support” is a single capability.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

A browser plays native video and supports canvas. A project labels it “HTML5 compatible” and plans to use an unrelated device API without further checks. What is missing from that reasoning?

What are `data-` attributes good for?

Topics
Web APIsHTMLTesting

TL;DR

data-* attributes attach small pieces of application-specific metadata to an element when no standard HTML attribute expresses the meaning. JavaScript reads and writes them through element.dataset. They are useful for component configuration, stable testing hooks, analytics labels, and IDs needed by event delegation, but they are strings in user-editable markup—not secrets, authorization state, or a replacement for a large application data model.


What are data- attributes good for?

Custom data attributes provide a standards-compliant bridge between HTML and code without inventing non-standard attributes.

Reading and writing values

A hyphenated attribute name maps to a camel-cased property on dataset:

<button type="button" data-product-id="sku-42" data-action="add-to-cart">
Add to cart
</button>
const button = document.querySelector('[data-action="add-to-cart"]');
console.log(button.dataset.productId); // "sku-42"
button.dataset.state = 'pending'; // Adds data-state="pending".

Values are exposed as strings. Parse numbers, booleans, or JSON explicitly and handle invalid input rather than relying on coercion.

Appropriate uses

  • Configure a reusable behavior directly from server-rendered markup.
  • Associate an element with an application identifier for event delegation.
  • Provide a stable data-testid when a test cannot locate an element by accessible role, label, or visible text.
  • Attach analytics metadata without reusing styling classes.
  • Integrate a library that defines a documented data-* convention.

When another mechanism is better

Use semantic elements and standard attributes first. For example, use disabled for an unavailable button and ARIA only when native HTML cannot express the accessibility semantics. Keep large or rapidly changing state in JavaScript or an external store rather than serializing it repeatedly into the DOM.

Anyone can inspect and modify the DOM, so never trust data-* values for permissions, prices, account identity, or security decisions. The server must validate any value that crosses a trust boundary. User editability is not a reason to avoid data-*; it is a reminder that client-side state is never authoritative.

Avoid using data attributes purely as styling hooks when a class communicates the styling role more clearly. Tests should prefer user-facing queries and use test IDs as a deliberate fallback.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

Which uses of data-* attributes are appropriate? Select all that apply.

What is progressive rendering?

Topics
HTML

TL;DR

Progressive rendering makes a page useful incrementally instead of waiting for every resource and component to finish. Deliver meaningful HTML early, prioritize the resources needed for the initial view, stream later content when appropriate, and defer or lazy-load noncritical work. Do not lazy-load the likely Largest Contentful Paint image, and measure real paint, layout-shift, and interaction metrics rather than equating “more lazy loading” with better performance.


What is progressive rendering?

Progressive rendering is a delivery strategy, not one browser API. The server and browser reveal useful content in stages while slower data, images, and JavaScript continue loading.

Send useful HTML early

Server-rendered or static HTML can display headings, navigation, and primary content before client JavaScript initializes. A server can also stream HTML as data becomes available, but the chunks should preserve document order, error handling, and a usable experience when JavaScript is slow or unavailable.

Placeholders should reserve the final component's space so later content does not cause unexpected layout shifts. A skeleton is helpful only when it communicates progress and does not replace content that could have been rendered immediately.

Prioritize critical resources

  • Discover required CSS and fonts early, but keep the critical path small.
  • Use defer or modules for application scripts that do not need to block parsing.
  • Preload only resources known to be required soon; unnecessary preloads compete for bandwidth.
  • Serve responsive images with intrinsic dimensions to avoid layout shifts.

Lazy-load noncritical content

Native image lazy loading is appropriate for images below the initial viewport:

<img
src="/gallery/photo-800.jpg"
alt="A mountain reflected in a lake"
width="800"
height="533"
loading="lazy" />

Do not set loading="lazy" on an image likely to be the page's Largest Contentful Paint element. For custom deferred UI, IntersectionObserver is more robust than handling every scroll event manually. Dynamic imports can delay noncritical JavaScript, but excessive splitting adds requests and loading states.

Framework techniques such as progressive or selective hydration are implementation options, not requirements of progressive rendering. Their benefit depends on how much JavaScript the page needs and when users can interact with it.

Measure the result

Use throttled Network and Performance recordings plus field data where available. First Contentful Paint and Largest Contentful Paint reveal display progress, Cumulative Layout Shift catches unstable staging, and Interaction to Next Paint helps show whether deferred JavaScript still blocks users later.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A news article page waits for recommendations, comments, and analytics before showing its main story. Describe a progressive rendering plan.

Why you would use a `srcset` attribute in an image tag?

Explain the process the browser uses when evaluating the content of this attribute.
Topics
HTML

TL;DR

srcset gives the browser multiple image candidates so it can choose an appropriate resource for the image's rendered size and device pixel density. With width descriptors such as 480w, also provide sizes so the browser can estimate the image's layout width before final layout is known. The selection is a browser decision and may consider network or cache state, so code should not depend on one exact candidate being chosen.

<img
src="/images/hero-800.jpg"
srcset="
/images/hero-480.jpg 480w,
/images/hero-800.jpg 800w,
/images/hero-1600.jpg 1600w
"
sizes="(max-width: 600px) 100vw, 800px"
width="800"
height="450"
alt="Team members collaborating around a table" />

Why you would use a srcset attribute in an image tag?

Responsive images avoid downloading more pixels than the layout can use while still providing enough detail for high-density displays.

Width descriptors and sizes

In the example above, each w descriptor states the file's intrinsic width. Before fetching an image, the browser approximately:

  1. Evaluates the first matching condition in sizes to determine the image's slot width in CSS pixels.
  2. Combines that slot width with device pixel density and other implementation factors.
  3. Selects a suitable candidate from srcset.
  4. Uses src as the fallback when srcset is unsupported.

The browser is allowed to use its own selection heuristics, so it may reuse a cached image or choose a different candidate under constrained conditions. The descriptors must match the files' real intrinsic widths. Without sizes, a width-descriptor source set defaults to assuming a 100vw slot, which often downloads an unnecessarily large image.

Pixel-density descriptors

If the image always renders at one fixed CSS size, density descriptors can provide resolution variants:

<img
src="/icons/logo.png"
srcset="/icons/logo.png 1x, /icons/logo@2x.png 2x"
width="160"
height="40"
alt="Example Company" />

Do not mix w and x descriptors in the same srcset.

Art direction uses <picture>

srcset normally offers different resolutions of the same image. When narrow screens need a different crop or composition, use <picture> with <source media> and keep an <img> fallback. The <img> still owns the alternative text and intrinsic dimensions.

Always provide meaningful alt text when the image conveys content and alt="" when it is decorative. Set width and height or an equivalent aspect ratio to reserve layout space. Lazy-load below-the-fold images, but do not lazy-load the likely Largest Contentful Paint image.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

An image uses width descriptors such as 480w, 800w, and 1600w. Why should it also provide sizes?

Difference between document `load` event and document `DOMContentLoaded` event?

Topics
HTMLJavaScript

TL;DR

DOMContentLoaded fires after the HTML has been parsed and deferred and module scripts have executed. It does not directly wait for images, subframes, or stylesheets, although a blocking stylesheet can delay a script and therefore indirectly delay DOMContentLoaded. The window load event waits for the document and its dependent resources, apart from resources loaded lazily.

document.addEventListener('DOMContentLoaded', function () {
console.log('DOM fully loaded and parsed');
});
window.addEventListener('load', function () {
console.log('Page fully loaded');
});

Document readiness timeline

DOMContentLoaded marks completion of DOM construction and deferred script execution; load waits longer for dependent resources such as images and stylesheets.

DOMContentLoaded and load timing

A slow image can delay load without delaying DOMContentLoaded; asynchronous scripts have separate timing rules.

Difference between document load event and document DOMContentLoaded event

DOMContentLoaded event

The DOMContentLoaded event fires after the initial HTML has been parsed and deferred and module scripts have run. It does not directly wait for images, subframes, or stylesheets. However, deferred scripts wait for blocking stylesheets, and DOMContentLoaded waits for those scripts, so stylesheets can delay it indirectly.

document.addEventListener('DOMContentLoaded', function () {
console.log('DOM fully loaded and parsed');
});

load event

The window load event fires when the document and its dependent resources, such as stylesheets, eagerly loaded images, scripts, and subframes, have finished loading. Lazily loaded resources do not hold it up. Use it only for work that actually requires those resources, such as reading intrinsic image dimensions.

window.addEventListener('load', function () {
console.log('Page fully loaded');
});

Key differences

  • Timing: DOMContentLoaded fires earlier than load. DOMContentLoaded occurs after the HTML is fully parsed, while load waits for all resources to be loaded.
  • Use cases: Use DOMContentLoaded for tasks that only require the DOM to be ready, such as attaching event listeners or manipulating the DOM. Use load for tasks that depend on all resources being fully loaded, such as image-dependent layout calculations.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A page needs to initialize a menu as soon as its markup and deferred scripts are ready. A large non-lazy hero image may still be downloading. Which event should the initializer use?

What kind of things must you be wary of when designing or developing for multilingual sites?

Topics
HTMLInternationalization

TL;DR

Multilingual design is more than translating strings. Use valid BCP 47 language tags, declare lang and dir in HTML, give users a persistent language switcher, keep whole messages translatable, format locale-sensitive values with Intl, and design for text expansion and different writing directions. Use stable locale-specific URLs and reciprocal hreflang links when localized pages should be indexed.


What kind of things must you be wary of when designing or developing for multilingual sites?

Internationalization prepares an application for multiple languages, scripts, regions, and cultural conventions. Localization supplies the content and rules for a particular audience.

Declare language and direction in markup

Set the document's default language on <html> and mark passages that switch languages. Screen readers, spellcheckers, search engines, and font selection can use this information.

<html lang="ar" dir="rtl">
<p>مرحبا</p>
<p lang="en" dir="ltr">Welcome</p>
</html>

Use BCP 47 tags such as en, en-GB, or zh-Hant. Language and direction are separate: lang does not automatically set dir. Use dir="auto" for user-generated text whose direction is unknown, and prefer CSS logical properties such as margin-inline-start over physical left and right properties.

Let users control the locale

Accept-Language can provide an initial hint, but it may reflect a shared device, proxy, or browser setting rather than the user's choice. IP geolocation is an even weaker language signal. Offer an obvious language switcher, preserve the selection, and avoid repeatedly forcing redirects.

Do not treat language, region, currency, and country as interchangeable. For example, a user may choose English while living in Japan and paying in Japanese yen.

Translate complete messages

Do not assemble sentences from translated fragments such as "The date is " + date. Word order, plural forms, grammatical gender, and surrounding punctuation differ between languages. Give translators complete messages with named placeholders and use the message-formatting features of the chosen localization system.

Format values with locale-aware APIs rather than handwritten patterns:

const price = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(1234.5);
console.log(price); // Formatting and spacing are locale-dependent.

Tests should assert the meaningful value or selected locale, not brittle punctuation that can vary between runtime data versions.

Design flexible layouts

  • Expect translated labels and headings to expand or wrap.
  • Avoid fixed heights for text containers and test long words, narrow viewports, and browser zoom.
  • Keep text as text instead of baking it into images.
  • Use fonts that cover the required scripts and verify fallback rendering.
  • Check focus order, keyboard interaction, form errors, accessible names, and truncation in every supported direction.
  • Do not communicate meaning through color, icons, or cultural metaphors alone.

Give localized pages stable URLs

Locale-specific paths such as /en/products and /fr/products are easier to share, cache, index, and debug than a single URL whose language changes silently. If pages are equivalent localized versions, add reciprocal rel="alternate" links with hreflang and optionally an x-default fallback. Use hyphenated BCP 47 values such as en-US, not locale-library identifiers such as en_US, in HTML.

Localization should be tested with actual target-language content and native or professional review; machine translation and pseudolocalization are useful checks but not final linguistic validation.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

A notification is assembled as t('You have') + ' ' + count + ' ' + t('new messages'). A translator needs a different word order and distinct forms for different counts. How should the translation boundary change?

How do you serve a page with content in multiple languages?

Topics
HTMLInternationalization

TL;DR

Give each localized page a stable URL, choose an initial locale from the URL, saved user preference, and optionally Accept-Language, then render one coherent language with the correct <html lang> and dir. Keep translations separate from templates, format locale-sensitive values with internationalization APIs, and let users switch languages. For indexable equivalents, publish reciprocal hreflang links.


How do you serve a page with content in multiple languages?

A robust implementation separates locale selection, translation data, rendering, and discovery by users and search engines.

Resolve the locale predictably

A common precedence is:

  1. An explicit locale in the URL, such as /fr/account.
  2. A saved user preference.
  3. A supported match from the request's Accept-Language header.
  4. The application's default locale.

Redirect to a locale-specific URL only when that behavior is intentional, and always let the user override it. Do not infer language solely from IP location.

If one URL varies its response by Accept-Language, caches must account for that variation, commonly through Vary: Accept-Language. Locale-specific URLs usually make caching, sharing, analytics, and search indexing simpler.

Render the selected language

The server can render localized HTML, the client can load a translation bundle, or a hybrid application can do both. Regardless of rendering architecture:

  • Load only supported translation catalogs and fall back deliberately for missing messages.
  • Escape translated content by default; translations are not trusted HTML merely because they came from a catalog.
  • Translate complete messages rather than concatenating fragments.
  • Use Intl or an internationalization library for dates, numbers, currencies, relative time, and plurals.
  • Keep one primary language per page and mark embedded passages with their own lang when necessary.
<!doctype html>
<html lang="de">
<head>
<link rel="alternate" hreflang="en" href="https://example.com/en/account" />
<link rel="alternate" hreflang="de" href="https://example.com/de/account" />
<link
rel="alternate"
hreflang="x-default"
href="https://example.com/account" />
</head>
<body>
<h1>Mein Konto</h1>
</body>
</html>

For a right-to-left document, also set dir="rtl". The lang attribute does not determine text direction.

Test the full delivery path

Check direct navigation, refreshes, shared URLs, cache behavior, missing translations, language switching, browser back/forward navigation, server and client hydration agreement, long translated strings, right-to-left layout, and accessibility output. A page that first renders one language and then switches after hydration creates visual instability and can produce hydration errors.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A site promises that locale-specific links preserve their language when shared. A visitor opens /fr/account, has a saved German preference, and sends Accept-Language: en. Which language should this direct navigation render?

What does a doctype do?

Topics
HTML

TL;DR

<!doctype html> is the required preamble for an HTML document. Its practical purpose in modern browsers is to select no-quirks mode so layout follows current web standards. It is not an HTML element, does not select an “HTML5 feature set,” and—unlike historical doctypes—does not reference a DTD.

<!doctype html>
<html lang="en">
<!-- Document content -->
</html>

What does a doctype do?

Browsers retain legacy rendering modes for compatibility with pages written around old browser behavior. A recognized modern doctype tells the browser to use no-quirks mode.

Rendering modes

  • No-quirks mode follows current HTML and CSS behavior.
  • Quirks mode emulates several legacy layout behaviors, including differences in the CSS box model.
  • Limited-quirks mode preserves a smaller set of legacy behaviors and can be triggered by certain historical doctypes.

The modern declaration is deliberately short and case-insensitive:

<!doctype html>

Place it before the <html> element. A missing or malformed declaration can put the document into quirks mode, producing layout differences that are difficult to diagnose.

Historical DTDs

Older HTML and XHTML doctypes sometimes included a public identifier and a DTD URL. The HTML Living Standard's doctype does not. Content-model rules—such as which elements may contain other elements—come from the HTML specification and are not selected at runtime by this declaration.

When debugging an old page, inspect the document mode in DevTools or check it directly:

console.log(document.compatMode);
// "CSS1Compat" in no-quirks mode; "BackCompat" in quirks mode.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

What is the practical purpose of <!doctype html> in a modern document?

Explain what a single page app is and how to make one SEO-friendly

Topics
JavaScriptHTML

TL;DR

A single page application (SPA) performs route transitions and view updates in the browser instead of loading a new HTML document for every navigation. A purely client-rendered SPA may return only an HTML shell initially, which can delay content discovery, metadata, and meaningful paint. SPA navigation does not require client-only initial rendering: the first route can be server-rendered or pre-rendered and then hydrated for client-side navigation.

For indexable routes, return meaningful HTML and correct status codes, canonical URLs, titles, metadata, structured data, and crawlable links. HTML can be produced per request (SSR), at build time or on demand (static generation/prerendering), or through a framework-specific cached regeneration model. Streaming can improve delivery but is not itself an SEO requirement. Test the rendered output and crawler behavior for the actual search engines and link-preview clients you support.


Search-friendly SPA delivery

A crawler-friendly architecture gives each route a stable URL and meaningful HTML and metadata before relying on client-side navigation.

SEO-friendly single-page application delivery

Client rendering can still enhance the page, but route discoverability should not depend on a crawler executing an empty application shell perfectly.

What is a single page app?

A SPA performs navigation and content updates on the client after the initial document load. That initial document can be a minimal client-rendered shell or fully rendered HTML that is later hydrated; the defining characteristic is client-side navigation, not an empty initial response.

Key characteristics:

  • Subsequent route transitions reuse the current document and update the view client-side.
  • The fetch API (or XMLHttpRequest) is used to communicate with the server without full-page reloads.
  • A client-side router (for example, react-router or vue-router) maps URL changes to view transitions.
  • Application state is typically held in memory rather than stored per request.

Benefits:

  • Smoother navigation after the initial load.
  • The browser can avoid full-document reloads, though server load depends on the API, rendering, caching, and data-fetching architecture.
  • Application-like interaction patterns, such as preserved state across route transitions.

How search engines render JavaScript

The statement "Google cannot index JavaScript" is out of date. The practical situation is more nuanced:

  • Googlebot executes JavaScript using an evergreen Chromium-based renderer. The bot fetches HTML first, and pages requiring JavaScript rendering are added to a separate render queue. The render queue has improved substantially since 2019, but indexing of JS-rendered content is typically slower than indexing of server-rendered HTML.
  • Rendering is separate from crawling. This two-phase model means JS-rendered content can appear in the index later than its server-rendered counterpart, which is a disadvantage for time-sensitive content or highly competitive queries.
  • Crawler capabilities and budgets vary. Do not assume every search engine or specialized crawler executes the same JavaScript successfully or on the same schedule.
  • Many social and preview scrapers rely on response HTML. Return important Open Graph and other preview metadata from the server rather than depending on client-side injection.

For details, see Google's JavaScript SEO documentation and the Google Search Central documentation on rendering.

Rendering strategies

Modern frameworks allow different strategies to be used on different routes within the same application.

Server-side rendering (SSR)

The server renders the HTML for each request using the current application state. This produces indexable HTML on first response and reflects up-to-date data, at the cost of per-request server rendering.

Example with the Next.js App Router:

// app/products/[id]/page.tsx — a React Server Component, async by default
export default async function ProductPage({ params }) {
const { id } = await params;
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: 'no-store',
});
const product = await res.json();
return (
<div>
<h1>{product.title}</h1>
<p>{product.description}</p>
</div>
);
}

The App Router, introduced in Next.js 13, replaces the getServerSideProps data-fetching function of the Pages Router with async Server Components. cache: 'no-store' opts out of the framework's data cache to produce a fresh render on each request.

Static site generation (SSG)

HTML is produced at build time and served as static files. This is the cheapest option to host, produces the fastest first-byte response, and is suitable for content that does not change per request.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) =>
r.json(),
);
return posts.map((p) => ({ slug: p.slug }));
}
export default async function Post({ params }) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>
r.json(),
);
return <article>{post.body}</article>;
}

Incremental static regeneration (ISR)

Pages are generated statically and cached, with a configurable revalidation interval. The cached HTML is served immediately; when the revalidation interval expires, the framework regenerates the page in the background on the next request. This combines SSG's serving cost with tunable freshness.

// app/categories/[slug]/page.tsx
export default async function Category({ params }) {
const { slug } = await params;
const res = await fetch(`https://api.example.com/categories/${slug}`, {
next: { revalidate: 3600 },
});
const category = await res.json();
return <CategoryView data={category} />;
}

React Server Components with streaming

React Server Components execute on the server and produce a serialized component payload that a framework can use alongside server-rendered HTML. Combined with Suspense boundaries and a streaming-capable framework, the server can send ready HTML earlier while slower sections continue rendering. React Server Components and HTML streaming are related framework features, not interchangeable rendering strategies.

import { Suspense } from 'react';
export default function Feed() {
return (
<>
<Header />
<Suspense fallback={<FeedSkeleton />}>
<SlowFeed />
</Suspense>
</>
);
}

Client-side rendering (CSR)

The HTML shell is served, and the full view is rendered by JavaScript in the browser. This remains appropriate for views that are not intended to be indexed, such as authenticated dashboards and internal tools, where SSR adds server cost without SEO benefit.

Choosing a strategy per route

The appropriate strategy depends on the route's data characteristics and indexing requirements. A typical allocation is:

Use caseRecommended strategyRationale
E-commerce product pageISR, short revalidation windowPrices and inventory change, but not per visit; cached HTML improves Largest Contentful Paint
Marketing site, documentation, blogSSGNo per-request variability; suitable for CDN distribution
Dashboard behind authenticationOften CSR; SSR can still help first loadUsually not indexed; choose based on latency, personalization, and server cost
Personalized feed or homepageRSC with streamingFast shell response; personalized content is streamed as it resolves
Search results pageSSRQuery-dependent output that should be indexable for long-tail queries
Real-time dashboardCSRData changes more frequently than server HTML can be regenerated usefully
Breaking news articleSSR or short-window ISRFreshness is important; SSR under traffic spikes, ISR otherwise

Core Web Vitals comparison

The rendering strategy affects both perceived performance and the Core Web Vitals users experience. The directions below are common tendencies, not guaranteed results; payload size, caching, server location, device speed, hydration work, and data waterfalls can reverse them.

StrategyTTFBLCPNotes
CSROften lowCan be highFast shell response; meaningful content may wait for JS and data
SSRCan be higherCan be lowerServer work affects TTFB; content may become visible sooner
SSGOften lowOften lowCacheable HTML, subject to asset and hydration costs
Cached regenerationOften low on cache hitsOften lowMiss and revalidation behavior is framework-specific
Streaming SSREarly shell possibleBoundary-dependentSlow boundaries need not hold back already completed HTML

Tools such as PageSpeed Insights and WebPageTest provide lab measurements against a specific URL.

Framework coverage

Several frameworks support the strategies above:

  • Next.js (React) — App Router is the current default. Supports SSR, SSG, ISR, and React Server Components with streaming.
  • Remix (React) — Emphasizes web standards and nested routing with loader and action functions. Merged with React Router in 2024.
  • Nuxt (Vue) — Supports SSR, SSG, ISR (via the Nitro server), and hybrid rendering.
  • SvelteKit (Svelte) — Adapter-based; deploys as SSR, SSG, or edge functions depending on configuration.
  • Astro — Island architecture. Ships zero JavaScript by default and hydrates only the components marked as interactive. Well suited to content-heavy sites with limited interactivity.
  • SolidStart (Solid) — Architecturally similar to SvelteKit with Solid's fine-grained reactivity.

General guidance for new projects:

  • Content-heavy sites with limited interactivity: Astro.
  • React applications with a mix of interactive and indexable routes: Next.js or Remix.
  • Vue applications: Nuxt.

A framework with SSR or SSG support is preferable to pure client-side rendering when SEO is a requirement, even for applications that would otherwise be implemented as a traditional SPA.

Common misconceptions

  1. "SSR means no JavaScript on the client." SSR produces server-rendered HTML, but the client still downloads and hydrates the JavaScript bundle to attach event handlers. The bundle size is generally comparable to the CSR equivalent.
  2. "CSR is not SEO-friendly." Googlebot indexes content rendered by JavaScript. The practical concerns are latency, indexing reliability, and compatibility with non-Google engines and social scrapers. For high-competition queries these concerns are significant; for long-tail content they may be acceptable.
  3. "SSR should be used for everything." SSR has a per-request CPU cost. For content that does not vary per request, SSG is substantially cheaper to serve. Defaulting to SSR when SSG would suffice increases hosting cost without benefit.
  4. "Hydration is free." Hydration re-executes the component tree on the client to attach event handlers. Large hydration trees can affect Interaction to Next Paint (INP) and other interactivity metrics. React Server Components reduce hydration cost by allowing portions of the tree to remain server-only.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which practices improve discoverability and sharing for indexable routes in a single-page application? Select all that apply.

When would you use `document.write()`?

Topics
Web APIsJavaScriptHTML

TL;DR

Almost never in new application code. During HTML parsing, document.write() injects markup into the input stream; after the document has loaded, it can implicitly call document.open() and replace the page. Its behavior in deferred or asynchronous scripts is problematic, it is an injection sink for untrusted strings, and browsers may intervene in slow-network cases.

You may encounter it in legacy scripts or tightly controlled parser-time snippets. Replace it with normal HTML, DOM creation methods, or explicit script loading. Use the console and debugger—not document.write()—for debugging.


Why parser timing matters

This writes while the parser is processing the document:

<script>
document.write('<p>Inserted while parsing</p>');
</script>

The result depends on when and how the script executes. Calling it later is destructive:

window.addEventListener('load', () => {
// This can erase the existing document. Do not do this.
document.write('<p>Replacement document</p>');
});

Safer alternatives

For text or DOM elements, create and append nodes:

const heading = document.createElement('h1');
heading.textContent = 'Hello, world!';
document.querySelector('#content').append(heading);

For static content, put the markup in HTML. For optional scripts, create a <script> element or use dynamic import(). If HTML from users is intentionally supported, process it with an appropriate sanitizer; do not pass it to document.write() or innerHTML directly.

Maintaining legacy usage

First determine whether the third-party script requires parser-time synchronous insertion. Test the replacement under slow networks, restrictive CSP, and supported browsers. If the call cannot yet be removed, ensure every string is developer-controlled, isolate the integration, and do not call it after parsing.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A diagnostic panel must append a status message after a network request finishes, well after the page has loaded. Which implementation preserves the existing document?

Describe the difference between `<script>`, `<script async>` and `<script defer>`