Five Browser APIs That Deserve More Attention
Some of the most practical browser APIs sit surprisingly low in developer awareness rankings. The 2021 State of JS Survey lists native JavaScript features by usage and awareness, and the poll for least-known APIs surfaces some genuinely useful tools. Four of them — the Page Visibility API, Web Share API, Broadcast Channel API, and Internationalization API — cover very different problems, yet each solves a real day-to-day need.
Page Visibility API
The Page Visibility API reports when a user actually leaves your page, whether they minimize the window or switch tabs. Before this API existed, developers improvised with blur and focus events:
window.addEventListener("focus", function () {
// User is back on the page
// Do Something
});
window.addEventListener("blur", function () {
// User left the page
// Do Something
});
That approach is unreliable. The blur event fires whenever the page loses focus — which also happens when the user clicks the browser console, an alert dialog or the window border. blur and focus only reveal whether the page is active, not whether its content is actually hidden.
Practical Applications
- Pausing videos, carousels or animations when the user leaves.
- Suspending live API polls while the page is not visible.
- Sending user analytics.
API Surface
The API exposes two properties and one event:
document.hidden— A global read-only property, now deprecated, returningtruewhen the page is hidden.document.visibilityState— The modern replacement, returning one of four values:visible(page is shown),hidden(page is minimized or in another tab),prerender(the starting state while prerendering), orunloaded(page is being freed from memory).visibilitychange— An event on thedocumentobject fired whenvisibilityStatechanges.
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
// page is visible
} else {
// page is hidden
}
});
To see it in action, scaffold a vanilla project with Vite:
npm create vite@latest unknown-web-apis
Select the vanilla framework when prompted, then install dependencies and start the dev server:
cd unknown-web-apis
npm install
npm run dev
Visit localhost:3000/ to confirm the project runs.
Clear the boilerplate from /main.js. Then, in /index.html, add a video element inside the #app div:
<div id="app">
<video controls id="video">
<source src="./yoshi.mp4" />
</video>
</div>
Back in /main.js, listen for visibilitychange on document and inspect document.visibilityState:
document.addEventListener("visibilitychange", () => {
console.log(document.visibilityState);
});
Inside that listener, select the video element with document.querySelector(), pause when the state is hidden and play when it returns to visible:
const video = document.querySelector("#video");
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
video.play();
} else {
video.pause();
}
});
Now the video halts whenever the user switches away. The same pattern stops wasteful network requests. Add a div to hold a quote:
<div id="app">
<video controls id="video">
<source src="./yoshi.mp4" />
</video>
<div id="quote"></div>
</div>
Then use the Fetch API against https://api.quotable.io/random:
const quote = document.querySelector("#quote");
const getQuote = async () => {
try {
const response = await fetch("https://api.quotable.io/random");
const {content, author, dateAdded} = await response.json();
const parsedQuote = ` <q>${content}</q> <br> <p>- ${author}</p><br> <p>Added on ${dateAdded}</p>`;
quote.innerHTML = parsedQuote;
} catch (error) {
console.error(error);
}
};
getQuote();
The getQuote async function awaits the fetch response, then parses the JSON. The quotable.io API returns content, author and dateAdded fields, which we inject into the quote div. Call it on an interval with setInterval():
const quote = document.querySelector("#quote");
const getQuote = async () => {
try {
const response = await fetch("https://api.quotable.io/random");
const {content, author, dateAdded} = await response.json();
const parsedQuote = ` <q>${content}</q> <br> <p>- ${author}</p><br> <p>Added on ${dateAdded}</p>`;
quote.innerHTML = parsedQuote;
} catch (error) {
console.error(error);
}
};
getQuote();
setInterval(getQuote, 10000);
Without a visibility check, that timer keeps hitting the network even when the page is in the background. Guard the fetch with a visibility test:
const getQuote = async () => {
if (document.visibilityState === "visible") {
try {
const response = await fetch("https://api.quotable.io/random");
const {content, author, dateAdded} = await response.json();
const parsedQuote = `
<q>${content}</q> <br>
<p>- ${author}</p><br>
<p>Added on ${dateAdded}</p>`;
quote.innerHTML = parsedQuote;
} catch (error) {
console.error(error);
}
}
};
getQuote();
setInterval(getQuote, 10000);
The Page Visibility API is widely supported across browsers.
Web Share API
Another low-awareness API with outsized utility is the Web Share API. It hands your page access to the operating system's native sharing mechanism — especially valuable on mobile, where users expect share sheets. You can distribute text, links and files without building or depending on third-party share widgets.
What To Use It For
The obvious use is letting visitors share your content to social platforms or copy it to the clipboard through the OS's own interface.
How It Works
Two interfaces make this possible:
navigator.canShare()— Takes the data you intend to share and returns a boolean indicating whether it is shareable.navigator.share()— Returns a promise resolving when the share succeeds. Invokes the native sharing UI and requires transient activation: the call must originate from user input like a button click. The data argument may include:url: the URL to share,text: text content,title: a title,files: an array ofFileobjects.
Reusing the quote example, add a share button to /index.html:
<div id="app">
<video controls id="video">
<source src="./yoshi.mp4" />
</video>
<div id="quote"></div>
<button type="button" id="share-button">Share Quote</button>
</div>
In /main.js, grab the button and write an async share handler:
const shareButton = document.querySelector("#share-button");
const shareQuote = async (shareData) => {
try {
await navigator.share(shareData);
} catch (error) {
console.error(error);
}
};
Attach a click listener. The text field reads quote.textContent and the url uses location.href:
const shareButton = document.querySelector("#share-button");
const shareQuote = async (shareData) => {
try {
await navigator.share(shareData);
} catch (error) {
console.error(error);
}
};
shareButton.addEventListener("click", () => {
let shareData = {
title: "A Beautiful Quote",
text: quote.textContent,
url: location.href,
};
shareQuote(shareData);
});
A caveat: the API only works in secure contexts, meaning pages served over https:// or wss://. Desktop and mobile browser support remains limited, so feature detection with navigator.canShare() is wise.
Broadcast Channel API
The Broadcast Channel API enables communication between browsing contexts — tabs, windows, iframes, or any place a page can render — as long as they share the same origin. Two contexts share an origin only if their URLs have identical protocols (http/https), domains (example.com), and ports (:8080). Without this API, cross-context messaging is blocked for security reasons.
Typical use cases involve keeping state synchronized across tabs or windows, both for user experience and security. For example, you can log a user in or out across all tabs at once, show newly uploaded assets in every open page, or signal a service worker to perform background tasks.
To establish communication, create a BroadcastChannel instance with a string identifier. Any context that creates a channel with the same name joins the same conversation.
const broadcast = new BroadcastChannel("new_channel");
The resulting object exposes two methods for messaging:
BroadcastChannel.postMessage()sends data to every connected context. It accepts any object type as its sole argument.
broadcast.postMessage("Example message");
BroadcastChannel.close()terminates the channel, letting the browser garbage-collect it since no further messages will arrive.
Incoming messages are delivered via a message event, which you can handle with addEventListener or the onmessage property. The event object's data property holds the payload, and additional fields — origin, lastEventId, source, and ports — identify the sender.
broadcast.onmessage = ({data, origin}) => {
console.log(`${origin} says ${data}`);
};
To demonstrate, extend the earlier quote app so a second page shows the same quote. Create a folder new-origin with its own /index.html and /main.js.
The HTML boilerplate contains a #quote div:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="../favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="quote"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
In /new-origin/main.js, set up a broadcast channel and grab that DOM element:
const broadcast = new BroadcastChannel("quote_channel");
const quote = document.querySelector("#quote");
Back in the original /main.js, create a channel named "quote_channel" and modify getQuote to broadcast each fetched quote:
const broadcast = new BroadcastChannel("quote_channel");
//...
const getQuote = async () => {
try {
const response = await fetch("https://api.quotable.io/random");
const {content, author, dateAdded} = await response.json();
const parsedQuote = ` <q>${content}</q> <br> <p>- ${author}</p><br> <p>Added on ${dateAdded}</p>`;
quote.innerHTML = parsedQuote;
broadcast.postMessage(parsedQuote);
} catch (error) {
console.error(error);
}
};
Then, in /new-origin/main.js, listen for message events and update quote.innerHTML whenever a new quote arrives:
const broadcast = new BroadcastChannel("quote_channel");
const quote = document.querySelector("#quote");
broadcast.onmessage = ({data}) => {
quote.innerHTML = data;
};
Open http://localhost:3000/new-origin/ alongside http://localhost:3000. Quotes now sync across both pages. Note that the original page only fetches new quotes while visible, so hidden tabs won't trigger updates.
Support: Widely supported across major browsers.
Internationalization API
Translating text isn't enough to serve a global audience — date, number, and unit formats differ by region and can easily cause confusion. The date "11/8/22" can mean November 8 in the US, August 11 in Europe and Latin America, or August 22, 2011 in parts of Asia and Canada. The Internationalization API (I18n API) handles these regional formatting differences.
The API works with locale identifiers: strings of hyphen-separated subtags describing language, script, region, and other preferences. For instance:
"zh"— Chinese (language)"zh-Hant"— Chinese written in traditional characters (script)"zh-Hant-TW"— Chinese in traditional characters as used in Taiwan (region)
These identifiers follow the RFC 5646 definition of language tags. The API exposes an Intl object with several constructors for language-sensitive data:
Intl.DateTimeFormat()— formats dates and timesIntl.DisplayNames()— formats language, region, and script namesIntl.Locale()— constructs and manipulates locale tagsIntl.NumberFormat()— formats numbersIntl.RelativeTimeFormat()— formats relative time descriptions
For the quote app, format the dateAdded field using Intl.DateTimeFormat(). Its constructor accepts a locale string and an options object. The resulting instance's format() method takes both a Date object and an options object.
const logDate = (locale) => {
const newDate = new Date("2022-10-24"); // YY/MM/DD
const dateTime = new Intl.DateTimeFormat(locale, {timeZone: "UTC"});
const formatedDate = dateTime.format(newDate);
console.log(formatedDate);
};
logDate("en-US"); // 10/24/2022
logDate("de-DE"); // 24.10.2022
logDate("zh-TW"); // 2022/10/24
Note the constructor's options sets timeZone: "UTC" to prevent shifting the date to the user's local timezone. Without it, a date like "10/23/2022" might be formatted differently depending on where the user is.
The formatted output changes by locale. To apply this to each quote, define a helper that converts a YYYY-MM-DD date string based on the user's preference, available via the navigator.language property.
const formatDate = (dateString) => {
const date = new Date(dateString);
const locale = navigator.language;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {timeZone: "UTC"});
return dateTimeFormat.format(date);
};
Call that helper inside getQuote() when rendering dateAdded:
const getQuote = async () => {
if (document.visibilityState === "visible") {
try {
const response = await fetch("https://api.quotable.io/random");
const {content, author, dateAdded} = await response.json();
const parsedQuote = `
<q>${content}</q> <br>
<p>- ${author}</p><br>
<p>Added on ${formatDate(dateAdded)}</p>`;
quote.innerHTML = parsedQuote;
broadcast.postMessage(parsedQuote);
} catch (error) {
console.error(error);
}
}
};
The quotes now display dates according to the visitor's locale setting. For a user with navigator.language equal to "en", dates appear as MM/DD/YY.
Support: Widely supported across major browsers.
Beyond the Basics
Both the Broadcast Channel API and the Internationalization API may rank low in awareness — appearing near the bottom in the State of JS Survey — but they are production-ready and solve real problems. Whether you're building multi-tab dashboards or serving localized content, these APIs reduce complexity and improve user experience without requiring external libraries. Their obscurity is an opportunity: plenty of similarly capable APIs remain undiscovered, and finding the right one can simplify your codebase considerably.




