HTML Web APIs
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://localhostalso 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:
| Event | Fires on | What you usually do |
|---|---|---|
dragstart | The dragged element | Store an id with dataTransfer.setData() |
dragover | A possible drop zone, many times a second | Call preventDefault() to allow dropping |
drop | The drop zone | Read the id with getData() and move the element |
dragend | The dragged element | Tidy up styles |
<!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.
<!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 withclearWatch()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().
<!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.
<!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:
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:
data: India 142 for 3 after 25 overs
event: wicket
data: {"batter": "Rohan", "score": "142/4"}
id: 57A 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.
| Technique | Direction | Good for |
|---|---|---|
fetch() | Browser asks, server answers once | Loading data on demand |
| Server-Sent Events | Server to browser only, continuous | Live scores, notifications, progress |
| WebSockets | Both directions, continuous | Chat, multiplayer games |
Both continuous techniques are explained in WebSockets & Server-Sent Events.
Common mistakes
- Forgetting
preventDefault()indragover. 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, orlocalhostwhile developing. - Storing objects directly in
localStorage. They become"[object Object]"; useJSON.stringify()andJSON.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,dragoverwithpreventDefault()anddrop; always add a non-drag alternative. - Geolocation needs HTTPS and the user's permission, and returns coordinates with an accuracy in metres.
localStoragekeeps strings across visits;sessionStoragekeeps 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
<!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?
By default nothing is a valid drop target; cancelling dragover marks the zone as one.
2. On which kind of page does the Geolocation API work?
Geolocation is a powerful feature, so browsers allow it only in secure contexts.
3. What is the main reason to use a Web Worker?
Workers run on a separate thread, so the main thread stays free for clicks and drawing.
4. Which technique fits a live score that only the server updates?
SSE keeps one connection open and pushes each new update from server to browser.
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
