HTML Web APIs

HTML · Lesson 39 of 44 9 min read Updated

Browsers can do far more than display HTML. They can find your location, remember data between visits, run heavy calculations in the background and receive live updates from a server. These features are called Web APIs: ready-made tools that the browser gives to your JavaScript. Older books call several of them "HTML5 APIs", because they arrived with HTML5.

This lesson is a tour of five classic ones: Drag and Drop, Geolocation, Web Storage, Web Workers and Server-Sent Events. Each needs a little JavaScript; you can come back to the code after the JavaScript course if some of it is new.

What a Web API is

An API (Application Programming Interface) is a set of objects and functions that one program offers to another. A Web API lives in the browser, usually on objects such as navigator, window or document. Three ideas apply to almost all of them:

  • Feature detection. Check that the API exists before using it, for example if ("geolocation" in navigator), and offer a fallback if it does not.
  • Secure contexts. Powerful features, including geolocation, only work on pages served over HTTPS. While you develop, http://localhost also counts as secure.
  • Permissions. Features that touch private data, such as location, ask the user first. The user can say no, so your page must still work without them.

Drag and Drop

The Drag and Drop API lets users pick up an element with the mouse and drop it somewhere else. Images, links and selected text can be dragged by default; any other element needs draggable="true".

A drag fires events on both ends:

EventFires onWhat you usually do
dragstartThe dragged elementStore an id with dataTransfer.setData()
dragoverA possible drop zone, many times a secondCall preventDefault() to allow dropping
dropThe drop zoneRead the id with getData() and move the element
dragendThe dragged elementTidy up styles
HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Packing list</title>
  <style>
    .zones { display: flex; gap: 12px; flex-wrap: wrap; }
    .zone { flex: 1; min-width: 180px; min-height: 160px; border: 2px dashed #999; padding: 8px; }
    .zone.over { background: #eaf6ea; border-color: seagreen; }
    li { list-style: none; padding: 6px 10px; margin: 4px 0; background: #fff3c4; cursor: grab; }
  </style>
</head>
<body>
  <h1>Trip to Kochi: packing list</h1>
  <div class="zones">
    <ul class="zone" id="todo" aria-label="To pack">
      <li draggable="true" id="charger">Phone charger</li>
      <li draggable="true" id="umbrella">Umbrella</li>
      <li draggable="true" id="tickets">Train tickets</li>
    </ul>
    <ul class="zone" id="packed" aria-label="Packed"></ul>
  </div>

  <script>
    for (const item of document.querySelectorAll("li")) {
      item.addEventListener("dragstart", (event) => {
        event.dataTransfer.setData("text/plain", item.id);
      });
    }
    for (const zone of document.querySelectorAll(".zone")) {
      zone.addEventListener("dragover", (event) => {
        event.preventDefault(); // without this, dropping is not allowed
        zone.classList.add("over");
      });
      zone.addEventListener("dragleave", () => zone.classList.remove("over"));
      zone.addEventListener("drop", (event) => {
        event.preventDefault();
        zone.classList.remove("over");
        const item = document.getElementById(event.dataTransfer.getData("text/plain"));
        if (item) zone.append(item); // ignore text dragged in from elsewhere
      });
    }
  </script>
</body>
</html>

Drag "Umbrella" to the right-hand box and drop it. The dataTransfer object carries the item's id from dragstart to drop. For privacy, the browser only lets you read that data in the drop event, not while the item is still moving.

The same drop event receives files dragged from the computer: they arrive in event.dataTransfer.files, ready to show or upload. Without your own drop handler, a dropped file simply opens in the browser tab.

Geolocation

The Geolocation API asks the device where it is. The answer comes from GPS, Wi-Fi networks or mobile towers, and includes an accuracy in metres. The browser shows a permission prompt the first time, and the page only gets an answer if the user allows it.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Where am I?</title>
</head>
<body>
  <h1>Nearest pickup point</h1>
  <button type="button" id="locate">Use my location</button>
  <p id="status">We only ask for your location when you press the button.</p>

  <script>
    const status = document.querySelector("#status");
    document.querySelector("#locate").addEventListener("click", () => {
      if (!("geolocation" in navigator)) {
        status.textContent = "This browser cannot share a location. Please type your PIN code instead.";
        return;
      }
      status.textContent = "Waiting for permission...";
      navigator.geolocation.getCurrentPosition(
        (position) => {
          const { latitude, longitude, accuracy } = position.coords;
          status.textContent = `You are near ${latitude.toFixed(4)}, ${longitude.toFixed(4)} (accurate to about ${Math.round(accuracy)} m).`;
        },
        (error) => {
          const reasons = { 1: "you did not allow location access", 2: "the position is unavailable", 3: "it took too long" };
          status.textContent = `No location: ${reasons[error.code] ?? error.message}. Please type your PIN code instead.`;
        },
        { enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 }
      );
    });
  </script>
</body>
</html>

getCurrentPosition() takes three arguments: a function for success, a function for failure, and options. timeout is how long to wait in milliseconds, and maximumAge allows a recent saved position, which is faster. If your browser blocks the prompt here, you will see the failure message instead, which is a good test of your error handling.

Some rules for using location responsibly:

  • Ask only after the user presses a button that explains why. A prompt on page load is often refused.
  • Always give a fallback, such as typing a city or PIN code.
  • To follow a moving user, such as a delivery rider, use watchPosition(), and stop it with clearWatch() when you are done.
  • A page inside an iframe from another site only gets location access if the parent allows it with allow="geolocation" on the <iframe>.

Web Storage

Web Storage keeps small amounts of text in the browser. localStorage keeps data until it is deleted, even after the browser is closed. sessionStorage keeps it only while that tab is open. Both store strings, so objects must be converted with JSON.stringify().

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Remember me</title>
</head>
<body>
  <label for="name">Your name</label>
  <input id="name">
  <button type="button" id="save">Remember me</button>
  <p id="greeting"></p>

  <script>
    const greeting = document.querySelector("#greeting");
    const visits = Number(localStorage.getItem("demo-visits") ?? 0) + 1;
    localStorage.setItem("demo-visits", visits);
    const name = localStorage.getItem("demo-name");
    greeting.textContent = name ? `Welcome back, ${name}! Visit number ${visits}.` : `Visit number ${visits}.`;

    document.querySelector("#save").addEventListener("click", () => {
      localStorage.setItem("demo-name", document.querySelector("#name").value);
      greeting.textContent = "Saved. Run the example again.";
    });
  </script>
</body>
</html>

Run it several times: the visit count goes up and your name is remembered. Storage belongs to one origin (scheme, host and port), so other websites cannot read it. Anyone using the same browser profile, and any script running on your page, can, so never keep passwords or private tokens there. The full lesson is localStorage & sessionStorage.

Web Workers

JavaScript on a page runs on one main thread, the same thread that handles clicks and drawing. A long calculation freezes the page until it finishes. A Web Worker runs a script on a separate thread, so the page stays responsive. Workers cannot touch the DOM; they talk to the page by sending messages with postMessage().

Normally the worker's code lives in its own file and you start it with new Worker("primes.js"). To keep this example in one page, the worker's code sits in a <script type="text/plain"> element and is turned into a file in memory.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Worker demo</title>
</head>
<body>
  <p>Count prime numbers up to two million (this can take a few seconds):</p>
  <button type="button" id="main">Count on the main thread</button>
  <button type="button" id="worker">Count in a worker</button>
  <p id="result"></p>
  <p>Keep clicking this while counting: <button type="button" id="tap">Tapped 0 times</button></p>

  <script type="text/plain" id="worker-code">
    onmessage = (event) => {
      let count = 0;
      for (let n = 2; n <= event.data; n++) {
        let prime = true;
        for (let d = 2; d * d <= n; d++) {
          if (n % d === 0) { prime = false; break; }
        }
        if (prime) count++;
      }
      postMessage(count);
    };
  </script>

  <script>
    const LIMIT = 2000000;
    const result = document.querySelector("#result");
    const code = document.querySelector("#worker-code").textContent;
    const worker = new Worker(URL.createObjectURL(new Blob([code], { type: "text/javascript" })));
    worker.onmessage = (event) => { result.textContent = `Worker found ${event.data} primes.`; };

    document.querySelector("#worker").addEventListener("click", () => {
      result.textContent = "Worker is counting...";
      worker.postMessage(LIMIT);
    });
    document.querySelector("#main").addEventListener("click", () => {
      let count = 0;
      for (let n = 2; n <= LIMIT; n++) {
        let prime = true;
        for (let d = 2; d * d <= n; d++) { if (n % d === 0) { prime = false; break; } }
        if (prime) count++;
      }
      result.textContent = `Main thread found ${count} primes.`;
    });
    let taps = 0;
    document.querySelector("#tap").addEventListener("click", (event) => {
      event.target.textContent = `Tapped ${++taps} times`;
    });
  </script>
</body>
</html>

Press "Count on the main thread" and then try the tap button: nothing responds until the count finishes. Press "Count in a worker" instead, and the tap button keeps working while the answer (148933) is calculated in the background. Learn more in Web Workers.

Server-Sent Events

Normally the browser asks and the server answers. With Server-Sent Events (SSE), the browser opens one long-lasting connection and the server keeps sending messages down it whenever something happens, such as a new cricket score or an order status change. You use the EventSource object:

JavaScript
const source = new EventSource("/live-score");

source.addEventListener("message", (event) => {
  console.log("Update:", event.data);
});

source.addEventListener("wicket", (event) => {
  console.log("Wicket!", JSON.parse(event.data));
});

// later, when the page no longer needs updates:
// source.close();

The server replies with the content type text/event-stream and writes plain text blocks, each ended by a blank line:

Text
data: India 142 for 3 after 25 overs

event: wicket
data: {"batter": "Rohan", "score": "142/4"}
id: 57

A block without an event: line arrives as a normal message event. If the connection drops, the browser reconnects by itself and sends the last id it saw, so the server can continue from there. SSE needs a real server, so this example cannot run in the editor.

TechniqueDirectionGood for
fetch()Browser asks, server answers onceLoading data on demand
Server-Sent EventsServer to browser only, continuousLive scores, notifications, progress
WebSocketsBoth directions, continuousChat, multiplayer games

Both continuous techniques are explained in WebSockets & Server-Sent Events.

Common mistakes

  • Forgetting preventDefault() in dragover. The drop zone then refuses every drop.
  • Asking for location as soon as the page loads. Ask after a clear button press, and always offer a fallback.
  • Testing geolocation on plain http://. It needs HTTPS, or localhost while developing.
  • Storing objects directly in localStorage. They become "[object Object]"; use JSON.stringify() and JSON.parse().
  • Trying to change the page from a worker. Workers have no DOM; send a message back instead.

Key points

  • Web APIs are browser features used from JavaScript; detect them before use and provide fallbacks.
  • Drag and Drop uses draggable, dragstart, dragover with preventDefault() and drop; always add a non-drag alternative.
  • Geolocation needs HTTPS and the user's permission, and returns coordinates with an accuracy in metres.
  • localStorage keeps strings across visits; sessionStorage keeps them for one tab.
  • Web Workers run heavy code off the main thread and communicate with messages.
  • Server-Sent Events stream updates from server to browser through EventSource.

Exercise

Extend the packing list from this lesson. Add a "Move" button inside each item that moves it to the other list, so that keyboard users can pack too. Then save the ids of the packed items in localStorage whenever the list changes, and restore them when the page loads.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Packing list</title>
  <style>
    .zone { min-height: 100px; border: 2px dashed #999; padding: 8px; }
    li { list-style: none; margin: 4px 0; background: #fff3c4; padding: 4px 8px; }
  </style>
</head>
<body>
  <ul class="zone" id="todo">
    <li draggable="true" id="charger">Phone charger <button type="button">Move</button></li>
    <li draggable="true" id="umbrella">Umbrella <button type="button">Move</button></li>
  </ul>
  <ul class="zone" id="packed"></ul>

  <script>
    const todo = document.querySelector("#todo");
    const packed = document.querySelector("#packed");
    const save = () => {
      const ids = [...packed.querySelectorAll("li")].map((li) => li.id);
      localStorage.setItem("packed-items", JSON.stringify(ids));
    };
    for (const id of JSON.parse(localStorage.getItem("packed-items") ?? "[]")) {
      const item = document.getElementById(id);
      if (item) packed.append(item);
    }
    for (const item of document.querySelectorAll("li")) {
      item.addEventListener("dragstart", (e) => e.dataTransfer.setData("text/plain", item.id));
      item.querySelector("button").addEventListener("click", () => {
        (item.parentElement === todo ? packed : todo).append(item);
        save();
      });
    }
    for (const zone of [todo, packed]) {
      zone.addEventListener("dragover", (e) => e.preventDefault());
      zone.addEventListener("drop", (e) => {
        e.preventDefault();
        zone.append(document.getElementById(e.dataTransfer.getData("text/plain")));
        save();
      });
    }
  </script>
</body>
</html>

The buttons give keyboard and touch users the same power as dragging. JSON.stringify() turns the array of ids into a string for storage, and JSON.parse() turns it back when the page loads.

Quick Quiz

1. Why must a drop zone call preventDefault() in its dragover handler?

2. On which kind of page does the Geolocation API work?

3. What is the main reason to use a Web Worker?

4. Which technique fits a live score that only the server updates?

Learn HTML with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call