I10 · Part 2 · Intermediate
Browser APIs, in depth
Storage, the URL bar, and the event system — past what the beginner DOM chapter reached.
09On this page
- Browser storage
- URL and URLSearchParams
- History and IntersectionObserver, briefly
- Events, in depth — live
- Delegation and custom events
- Clipboard — reading and writing outside the page
- Page Visibility — knowing when nobody's looking
- BroadcastChannel — talking to your own other tabs
- Web Share and Geolocation, briefly
Browser storage
localStorage |
sessionStorage |
|
|---|---|---|
| Survives | closing the tab, the browser, the computer restarting | only this tab; gone when it closes |
| Shared across tabs? | yes, same origin | no — each tab gets its own |
| Capacity | ~5-10MB, string values only | same |
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark" — always a string
localStorage.setItem("user", JSON.stringify({ name: "Ana" }));
JSON.parse(localStorage.getItem("user")); // objects need to round-trip through JSON yourself
localStorage.removeItem("theme");
URL and URLSearchParams
const url = new URL("https://shop.example.com/search?q=js&page=2");
console.log(url.pathname); // what happens?
console.log(url.searchParams.get("q")); // what happens?
url.searchParams.set("page", "3");
console.log(url.toString()); // what happens?
A parsed URL gives every piece
(pathname, hostname, protocol)
as its own property, and searchParams is a live,
mutable view — editing it and reading url.toString()
again reflects the change immediately, no manual query-string
concatenation required.
Dry run: parsing and mutating the URL step by step
| Step | Code | Result |
|---|---|---|
| 1 | new URL("https://shop.example.com/search?q=js&page=2") | parses into pieces — pathname is "/search", searchParams holds q=js and page=2 |
| 2 | url.pathname | "/search" — just the path, no query string |
| 3 | url.searchParams.get("q") | "js" |
| 4 | url.searchParams.set("page", "3") | mutates the live searchParams view in place — page goes from 2 to 3 |
| 5 | url.toString() | "https://shop.example.com/search?q=js&page=3" — the change shows up automatically |
Step 4 never touches url.href or url.search directly — searchParams is a live view onto the same URL object, so mutating it is enough for step 5's toString() to reflect the change.
History and IntersectionObserver, briefly
history.pushState({ page: 2 }, "", "/products?page=2"); // changes the URL bar, no page reload
window.addEventListener("popstate", (e) => {
console.log("back/forward pressed, state:", e.state); // fires on browser back/forward, not on pushState itself
});
This is the mechanism every client-side router (React Router, Next.js's own routing) is built on — a URL that changes without a real navigation, plus a way to hear when the user manually goes back or forward.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) console.log(entry.target, "scrolled into view");
});
});
document.querySelectorAll(".lazy-image").forEach((img) => observer.observe(img));
The standard, efficient way to know when an element enters or leaves
the viewport — infinite scroll, lazy-loaded images, and "animate in
on scroll" effects all run on this instead of a
scroll listener doing math on every single pixel of
scrolling.
Events, in depth — live
Click the innermost box below and watch the log. Every listener here
is a real addEventListener call against the actual
nested boxes on this page.
outer
middle
inner — click me
With the capture checkbox off (the default), clicking "inner" logs
inner → middle → outer — the event starts at the exact
element clicked and bubbles upward through every ancestor
listening for it. Check the capture box and it reverses to
outer → middle → inner — capture-phase listeners run on
the way down, before the event even reaches its target.
Check "stopPropagation" and only the inner listener fires at all —
the click never continues past it in either direction.
Delegation and custom events
Bubbling is what makes event delegation work: one listener on
a parent container, instead of one per child, checking
event.target to see which child was actually clicked.
list.addEventListener("click", (e) => {
const item = e.target.closest("li"); // works even if the click landed on a span INSIDE the li
if (!item) return;
console.log("clicked:", item.dataset.id);
});
// one listener handles every current AND future <li> — no re-binding when items are added later
const updated = new CustomEvent("cart:updated", { detail: { count: 3 } });
cartElement.dispatchEvent(updated);
cartElement.addEventListener("cart:updated", (e) => {
console.log("new count:", e.detail.count);
});
A custom event bubbles and can be listened for exactly like a real browser event — the standard way for one part of a page to announce something happened without being directly wired to whoever might care.
Clipboard — reading and writing outside the page
await navigator.clipboard.writeText("copied!"); // should run from a real user gesture (a click), not on page load
button.addEventListener("click", async () => {
const text = await navigator.clipboard.readText(); // asks permission the first time
console.log("clipboard had:", text);
});
Both methods are async and both are gated behind a
secure context (HTTPS or localhost) and, for anything beyond a
same-origin write, a permission prompt. Reading the clipboard
specifically is the more restricted half — a page silently reading
whatever's on a user's clipboard on load would be a real privacy
problem, so the browser insists on both a secure context and, in most
browsers, the read happening inside a direct response to user input.
Page Visibility — knowing when nobody's looking
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
pauseVideo();
stopPolling();
} else {
resumePolling();
}
});
A tab that's minimized, in the background, or on a phone whose screen
just locked doesn't stop running JavaScript — timers keep firing —
but the user isn't watching. visibilitychange is the
standard hook for pausing expensive work (video, polling, animation)
exactly then, and resuming it the moment the tab becomes visible
again, rather than burning battery and bandwidth on a tab nobody's
looking at.
BroadcastChannel — talking to your own other tabs
const channel = new BroadcastChannel("cart-updates");
channel.postMessage({ itemCount: 3 }); // every OTHER tab/window on the same origin hears this
channel.addEventListener("message", (event) => {
console.log("another tab said:", event.data);
});
Same-origin tabs can already coordinate through the
storage event (which fires when another tab writes to
localStorage), but that only works as a side effect of
actually storing something. BroadcastChannel is built
for exactly this job directly — send a message to every other open
tab of the same site, with no storage write required at all. Closing
every tab connected to a channel is the only cleanup needed; there's
no server, no polling, just same-origin tabs talking directly.
Web Share and Geolocation, briefly
await navigator.share({ title: "Check this out", url: location.href }); // hands off to the OS's native share sheet
navigator.geolocation.getCurrentPosition(
(pos) => console.log(pos.coords.latitude, pos.coords.longitude),
(err) => console.log("denied or unavailable:", err.message)
);
Both need a real user gesture and a secure context, and both are
permission-gated the same way clipboard reads are.
navigator.share only exists where the OS has a native
share sheet to hand off to — always feature-detect
(if (navigator.share)) and fall back to your own share
buttons where it's missing.
Interview answer, one sentence
"The browser hands you storage, URLs, observers, the clipboard, cross-tab messaging and visibility as APIs, and the skill is picking the smallest one that fits — URLSearchParams over string slicing, IntersectionObserver over scroll listeners, delegation over a listener per element — while remembering many are asynchronous or permission-gated."
Before you move on
- Explain why editing
url.searchParamsand callingurl.toString()reflects the change immediately, with no manual string concatenation. - Predict the click log order in the bubbling demo with the capture checkbox on versus off, and say why capture-phase listeners fire first.
- Explain what
event.stopPropagation()actually stops, and why event delegation depends on propagation still working elsewhere. - Explain why
visibilitychange, not a periodic check, is the right hook for pausing video or polling when a tab isn't visible. - Name two APIs from this chapter that require both a secure context and a real user gesture, and explain why that restriction exists.
Opens in the editor: write it, run it, and check it against real tests.
All 33 for this level →Finished reading?
0 of 41 chapters read in this series