Device & platform APIs
Use browser device APIs responsibly: clipboard, permissions, geolocation, notifications, and audio or video controls that depend on user trust.
- 01Ask at the right timeCheck that the browser has a feature, and ask for permission only after the person clicks.
- 02Handle a noShow a helpful message or another way for each permission state and geolocation error, instead of treating no as a failure.
- 03Control sound and videoUse
play()and its promise, media events,volume, andplaybackRateon audio and video elements.
Device APIs need the person's trust
An API is a set of objects and functions that the browser gives your code. So far, your JavaScript has changed data, the page, forms, storage, and network requests. Device APIs reach further, into the person's own device. With them, a page can:
- copy text to the clipboard, the place where copied text waits until you paste it, and read it back;
- ask where the device is (this is called geolocation);
- show notifications, the small messages that pop up from the operating system;
- play sound and video.
These are useful. They also touch the person's privacy and attention, so the browser guards them. Good code follows the same steps every time:
- Check that this browser has the feature.
- Say why the page needs it.
- Wait until the person clicks or acts.
- Handle a "no".
- Keep another way to finish the task.
This lesson uses events from Introduction to events and promises from Promises. It also uses a Blob from the Files & Blobs lesson. A Blob is a chunk of raw data, like a small file made in code.
A hotel does not give every guest a key to every door. Each card opens only the doors it was made for. Browser permissions work the same way. Each feature needs its own yes, and the person can always say no.
- In real life: Your card opens your room
- In JavaScript: Allowing location lets the site use location
- In real life: Other doors stay locked
- In JavaScript: Allowing location does not allow notifications or clipboard reads
- In real life: You can turn down the card
- In JavaScript: The person can choose
denied, or ignore the question - In real life: Staff say what the card is for
- In JavaScript: A good page says why it asks, right when you want the feature
Every experiment in this lesson starts when you click a button, paste text, or press a play control. Nothing asks for the clipboard, your location, notifications, or sound when the page loads.
Secure pages and feature checks
TRUST FIRSTMost browsers allow these APIs only in a secure context. That means a page that came over HTTPS, or from localhost while you build on your own computer. HTTPS stops other people on the network from changing the page on its way to you. Without it, an attacker could change a page that asks for your location. No library or framework can skip this rule; the browser decides.
Browsers also differ in which APIs they have. This matters in React. React often builds the page first on a server, and the server has no navigator, no window, no permissions, and no speakers. So check for browser features only in the browser:
- after the page has loaded there (React calls this after mount), or
- inside a click handler.
If the server guesses one answer and the browser finds another, React sees two different pages. It then shows a warning called a hydration mismatch.
The step-through below is a small model of a good permission request. It shows the order: name the feature, see that the person has not decided yet, wait for a click, and say why before the real request.
Pick a feature. Guess which messages print, then step through.
script
const state = "prompt"; if (state === "prompt") { console.log("wait for a click before requesting " + feature);} console.log("explain why the app needs " + feature);A feature check asks a smaller question than a permission does: "Does this browser have this function?" Check before you turn a button on. The code below uses a made-up browser object in place of the real navigator, so it gives the same answers everywhere.
const browser = { mediaDevices: { getUserMedia() {} }, vibrate: true, getBattery: false,}; console.log(typeof browser.mediaDevices?.getUserMedia === "function");console.log(browser.vibrate === true);console.log(typeof browser.getBattery === "function");Line 7 uses ?. (optional chaining). If mediaDevices were missing, it would give undefined instead of an error. typeof gives the type of a value as text, so typeof x === "function" is true only for a function. In plain words, line by line:
- Line 7 prints
true.getUserMediais a function, so the page could ask for the microphone. - Line 8 prints
true. In this model,vibrateistrue. - Line 9 prints
false.getBatteryisfalse, not a function, so battery info is missing.
browser object. getUserMedia and vibrate are there, so they print true; getBattery is not a function, so it prints false.Having a function is not the same as having permission. The function can exist, and the person can still say no.
Clipboard API: copy, paste, and read
INTERACTIVEThe Clipboard API lives at navigator.clipboard. Its most common use is copying text after a click, with writeText().
writeText() returns a promise. A promise is a value that later says "done" (it resolves) or "failed" (it rejects). So show "Copied" only after it resolves, and show the real error if it rejects.
Some browsers let a page write to the clipboard only right after the person clicks or presses a key. So call writeText() directly inside the click handler, not later.
Your page does not own the clipboard. Other apps use it too. So the browser makes your page show that the person really asked to put something there, or to take something from it.
- In real life: Put a note on the tray
- In JavaScript:
navigator.clipboard.writeText(text) - In real life: Pick up what is on the tray
- In JavaScript:
navigator.clipboard.readText() - In real life: The person hands you the note
- In JavaScript: A
pasteevent withevent.clipboardData
const text = messageInput.value;await navigator.clipboard.writeText(text);status.textContent = "Copied!"; pasteBox.addEventListener("paste", (event) => { status.textContent = event.clipboardData.getData("text");}); const pasted = await navigator.clipboard.readText();(empty)Click Copy text, paste into the field, or try Read clipboard.
The lab has two ways to read text:
- The paste field. You choose Paste yourself, so the browser fires a
pasteevent. The event carries the pasted text inevent.clipboardData. - The Read clipboard button. It calls
readText(), which asks the browser for whatever is on the clipboard right now. That is more private, so most browsers show a question or a small Paste menu first.
Check a permission without asking
DASHBOARDA permission is the person's yes or no for one feature on one site. navigator.permissions.query() reads the current answer without showing the person a question. That helps you:
- turn a button on or off;
- show a helpful note next to a feature;
- build a dashboard like the one below.
Not every browser knows every permission name. An unknown name can throw a TypeError. So the code catches that error, and you still check the feature itself.
const names = ["geolocation", "notifications", "camera"]; for (const name of names) { try { const status = await navigator.permissions.query({ name }); render(name, status.state); status.addEventListener("change", () => render(name, status.state)); } catch (error) { render(name, "unsupported"); }}This dashboard calls permissions.query() after the page loads. It never asks you for anything. It only shows what the browser is willing to tell.
TypeError for names they don't know.Each row below is one state that query() can report. Read across to see what it means and what your page should do.
| State | What does it mean? | What should the page do? |
|---|---|---|
granted | The person said yes, so this site may use the feature. | Use the feature, and still show clear buttons to stop it. |
prompt | The person has not decided yet, or the browser won't tell you the answer in advance. | Wait until the person clicks the feature. Then ask. |
denied | The person or the browser said no. | Don't ask again. Say how to change the setting, and offer another way. |
| Unknown name | Some browsers throw a TypeError for permission names they don't know. | Catch the error, and check the feature itself another way. |
granted means use the feature. prompt means ask only after a click, and denied means stop asking and offer another way, like typing a city.A denied answer should change what the page offers. The page should not ask again and again. The code below keeps the task useful: when location is denied, the person types a city instead.
const permission = "denied";const message = permission === "granted" ? "Use device location" : "Let the person type a city"; console.log(message);In plain words, line by line:
- Line 1 sets the state to
"denied". - Lines 2 to 4 pick a message. Only
"granted"gets the device location; every other state gets line 4. - Line 6 prints
Let the person type a city.
query() gives back a PermissionStatus object. It fires a change event when the person changes a browser setting while your page is open.
Remember: prompt does not mean yes. It means "not decided yet". Ask later, when the person wants this feature.
Geolocation: ask where the device is
INTERACTIVEnavigator.geolocation.getCurrentPosition() asks where the device is. It takes three arguments:
- a success callback (a function the browser calls later), which gets the position;
- an error callback, which gets an error with a number in
code; - an options object.
The position holds latitude, longitude, and accuracy. Accuracy is in meters: how far off the answer might be.
accuracy in meters; error gets a code from 1 to 3.The options let you choose:
enableHighAccuracy: try for a more exact answer. It may be slower or use more battery.timeout: how many milliseconds to wait before giving up.maximumAge: how old a saved answer may be, in milliseconds.
You can ask politely, but you can't demand a perfect answer. Good apps plan for a refusal and for a rough answer.
- In real life: They don't want to answer
- In JavaScript: Error code 1:
PERMISSION_DENIED - In real life: They point roughly down the street
- In JavaScript: Coordinates plus an
accuracyin meters - In real life: They take too long
- In JavaScript: Error code 3:
TIMEOUT - In real life: They repeat what they told you a minute ago
- In JavaScript:
maximumAgeallows a saved answer
navigator.geolocation.getCurrentPosition( (position) => { const { latitude, longitude, accuracy } = position.coords; show(formatCoordinates(latitude, longitude, accuracy)); }, (error) => show(mapGeolocationError(error.code)), { enableHighAccuracy: true, timeout: 8000, maximumAge: 60000 },);Click Share my location. If you'd rather not share, click Simulate Mumbai result.
Click Share my location. If you'd rather not share, click Simulate Mumbai result.
This lesson rounds coordinates to two decimal places so they are easy to read. A real app should decide what it needs: an exact location, a rough area, or a city the person types. The lab never saves or sends your coordinates. If you don't want to share, use Simulate Mumbai result.
Notifications: ask only when the person wants them
CLICK ONLYThe Notifications API keeps its own permission in Notification.permission. It has one of three values: granted, denied, or default (not decided yet).
To ask, call Notification.requestPermission(). Call it only after the person chooses a notification feature, such as a "Remind me" button.
if (Notification.permission === "default") { const permission = await Notification.requestPermission(); if (permission === "granted") { new Notification("Device APIs lab", { body: "Thanks for opting in." }); }}Check another way
Click the button only if you want to test notifications. It asks for permission first.
A page can show a test notification while it is open. Real apps usually send notifications from a service worker, a script that keeps running in the background, so messages arrive even when the page is not in front. Some platforms limit web notifications even more, for example to web apps that the person installed. When browsers differ like this, say "in most browsers", not "always".
Audio and video share one API
LIVE AUDIOAn <audio> or <video> element is an HTMLMediaElement. Both have the same tools:
- methods:
play()andpause(); - properties:
currentTime(seconds played),volume(0 to 1), andplaybackRate(speed, where 1 is normal); - events:
play,pause,timeupdate(the time moved on), andended.
You send commands, and the TV tells you whether they worked. In the same way, play() gives you a promise, and media events keep your buttons and clock up to date.
- In real life: Press Play
- In JavaScript: Call
audio.play()and wait for its promise - In real life: The TV won't start with sound
- In JavaScript: The promise can reject with
NotAllowedError - In real life: Change the volume or speed
- In JavaScript: Set
volumeorplaybackRate - In real life: The TV shows what happened
- In JavaScript: Listen for
play,pause,timeupdate, andended
play() gives back a promise. If it resolves, line 5 logs play() resolved; if the browser blocks the sound, line 7 logs the error name, often NotAllowedError.const audio = document.querySelector("audio"); try { await audio.play(); log("play() resolved");} catch (error) { log(error.name);} audio.volume = 0.4;audio.playbackRate = 1.25;audio.addEventListener("timeupdate", updateClock);0.351×0.00s- The page makes a short sound file after it loads.
currentTime is 0.00s. The page makes a short sound file after it loads.
To use the camera or microphone, call navigator.mediaDevices.getUserMedia(). It needs a secure page and the person's permission. So ask from a button, and stop each track (one stream of sound or video) when you no longer need it. You can't run the code below here, because its result depends on a real microphone and the browser's question.
async function askForMicrophone() { if (!navigator.mediaDevices?.getUserMedia) { throw new Error("Microphone capture is unavailable here."); } return navigator.mediaDevices.getUserMedia({ audio: true });} button.addEventListener("click", async () => { try { const stream = await askForMicrophone(); status.textContent = "Microphone ready."; stream.getTracks().forEach((track) => track.stop()); } catch (error) { status.textContent = error instanceof Error ? error.name : "Microphone unavailable."; }});In plain words, line by line:
- Line 2 checks that
getUserMediaexists. If not, line 3 throws an error. - Line 5 asks for sound only:
{ audio: true }. - Line 8 waits for a click on the button.
- Line 10 waits for the person's answer.
- Line 12 stops every track, so the microphone turns off again.
Vibration is a small extra signal, and many devices don't have it. Check with "vibrate" in navigator first, and respect people who turn off motion. Battery info is missing even more often: navigator.getBattery() may not exist, or may be blocked. Never make the main task depend on the battery level.
if ("vibrate" in navigator) { navigator.vibrate(200);} if ("getBattery" in navigator) { const battery = await navigator.getBattery(); console.log(Math.round(battery.level * 100) + "%");} else { console.log("Battery status is unavailable.");}In plain words, line by line:
- Lines 1 to 3 vibrate for 200 milliseconds, but only if
vibrateexists. - Lines 5 to 7 read the battery and print its level as a percent.
- Line 9 prints a message when battery info is missing.
The media lab makes a tiny WAV sound file inside the page. It plays it from a Blob URL, a short address that points to data made in the page, so nothing is downloaded.
The Play button matters because of autoplay rules. In most browsers, starting sound without a click makes play() reject with NotAllowedError. A video element works the same way.
Where you'll use this
SORTERReal tasks start as plain sentences, not API names: "copy the invite link", "show nearby classes", "remind me when the export is done", or "slow down the video". For each task below, ask what it touches. Then ask what permission or click it needs.
- Copy an invite link after the person presses Share
- Let a learner paste a coupon code into a field
- Find workshops near the reader’s current city
- Show “about 30 m accuracy” beside a map dot
- Remind someone when a long export finishes
- Ask once before sending calendar reminders
- Play a short chime made in the page
- Let a learner slow a tutorial video to 0.75×
Choose the API for each real task.
- Does the app still work if the person says no?
- Can this start from a click, a paste, or a play control?
- Does this browser have the API, on a secure page?
- Which error or permission state will the page show?
Common misconceptions
PITFALLS- "One permission covers everything." Each feature has its own permission, usually per site. Allowing location does not allow notifications.
- "
promptmeans I can use it." It means the person has not decided yet. Ask only when they choose the feature. - "
permissions.querytells me everything." Browsers know different names, and an unknown name can throw. Check the feature too. - "Geolocation is exact." The answer comes with an accuracy in meters. It can also be old, missing, or denied.
- "
play()always starts the sound." Its promise can reject because of autoplay rules, missing data, or a file type the browser can't play. - "Reading the clipboard is the same as a paste." In a paste, the person chose to hand you the text.
readText()takes it without that step, so browsers guard it more.
Practice exercises
5 EXERCISESGuess the exact text this geolocation error function prints.
function messageFor(code) {
if (code === 1) return "Permission denied";
if (code === 2) return "Position unavailable";
if (code === 3) return "Timeout";
return "Unknown";
}
console.log(messageFor(3));Code 3 means TIMEOUT, so messageFor(3) returns and prints Timeout.
Guess what the code prints for the Mumbai coordinates.
function rounded(lat, lon) {
return lat.toFixed(2) + ", " + lon.toFixed(2);
}
console.log(rounded(19.076, 72.8777));19.076 rounds to 19.08, and 72.8777 rounds to 72.88. So the code prints 19.08, 72.88.
Guess the message for the denied state.
function shouldAsk(state) {
return state === "prompt" ? "ask on click" : "do not prompt";
}
console.log(shouldAsk("denied"));The function returns do not prompt for every state except prompt. After a no, the page should explain or offer another way, not ask again.
Write the missing code for a Copy button, in your editor or the console. Keep the copy inside the click handler.
button.addEventListener("click", async () => {
// copy inviteLink here
});button.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(inviteLink);
status.textContent = "Invite link copied.";
} catch (error) {
status.textContent = error instanceof Error ? error.message : "Copy failed.";
}
});The copy happens inside the click handler, right after the click. The page shows a message whether the copy works or fails.
Fix this function so a blocked play() shows a message on the page.
async function startAudio(audio, status) {
await audio.play();
status.textContent = "Playing";
}async function startAudio(audio, status) {
try {
await audio.play();
status.textContent = "Playing";
} catch (error) {
status.textContent = error instanceof Error ? error.name : "Playback blocked";
}
}A rejected play() promise is normal browser behavior. The catch shows the error name, so the button doesn't just silently fail.
Check your understanding
7 QUESTIONSQuestion 1 of 7Where do most browsers allow these device APIs?
Choose an answer to see the explanation.
Question 2 of 7What does this code print for Mumbai?
Read the code, then predictfunction formatCoordinates(latitude, longitude, accuracy) { return latitude.toFixed(2) + ", " + longitude.toFixed(2) + " (about " + Math.round(accuracy) + " m accuracy)"; } console.log(formatCoordinates(19.076, 72.8777, 28.6));Choose an answer to see the explanation.
Question 3 of 7Which geolocation error code means the time ran out?
Choose an answer to see the explanation.
Question 4 of 7What should happen before calling
Notification.requestPermission()?Choose an answer to see the explanation.
Question 5 of 7What does
play()give back on an audio or video element?Choose an answer to see the explanation.
Question 6 of 7What does this code print?
Read the code, then predictfunction permissionBadge(state) { return state === "granted" ? "Allowed" : state === "denied" ? "Blocked" : state === "prompt" ? "Ask on use" : "Check another way"; } console.log(permissionBadge("unsupported"));Choose an answer to see the explanation.
Question 7 of 7A Paste button reads the clipboard text after a click. Which API does it use?
Choose an answer to see the explanation.
Key takeaways
- Most device APIs work only on secure pages (HTTPS or
localhost). Check that the browser has the feature before you use it. - Ask for a permission only after the person clicks, never when the page loads.
- Clipboard reads, geolocation, notifications, and media playback can all fail in normal use. Plan for it.
- Geolocation gives an accuracy in meters and has three error codes. Never assume the answer is exact.
HTMLMediaElementruns both audio and video, with a promise fromplay(), properties, and events.
Final definition.
Device APIs let a page use parts of the person's device, like the clipboard, location, notifications, and sound. The browser guards them, so good code checks for the feature, asks at the right moment, and still works when the answer is no.
Up next: Web Workers.