šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

The Permissions API: Checking Before You Ask

Master navigator.permissions.query(): reading granted/denied/prompt state without triggering a dialog, designing UX around each state, reacting to live changes, and understanding its role as a UX layer rather than the actual security enforcement.

⚔ Total XP: 0|šŸ’» html XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

Permissions API

Query before you prompt.


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Every unnecessary or badly-timed permission prompt trains users to reflexively deny the next one. The Permissions API lets an application check current state and design deliberate UX around it, instead of firing a browser prompt the instant a page loads.

1query(): A Pure Read, Never A Prompt

navigator.permissions.query({ name: "geolocation" }) returns a promise resolving to a PermissionStatus object whose .state property is one of three values: "granted" (already allowed), "denied" (explicitly blocked), or "prompt" (undecided — the browser would show its native permission dialog if the feature were actually requested). Calling query() itself never shows any UI to the user; it purely reports the current state.

This distinction — checking versus requesting — is the entire value of the API. Without it, the only way to learn a permission's state was to actually attempt to use the feature, which for many permission-gated APIs is indistinguishable from requesting it, meaning you couldn't check state without risking triggering (or re-triggering) a prompt.

const status = await navigator.permissions.query({ name: "geolocation" });
console.log(status.state); // "granted" | "denied" | "prompt"
localhost:3000
āœ“ Read-Only, Zero Side Effectsquery() reports state without ever showing the user a permission dialog on its own.

2Designing Distinct UX For Each State

The real value of knowing state ahead of time is being able to design a genuinely different experience for each case, rather than firing the same prompt blindly. "granted" means you can proceed directly, with no explanatory UI needed. "prompt" is the moment to show context first — a brief explanation of *why* the feature needs this access, presented as an in-page UI element the user chooses to interact with, immediately before the actual request (which is what most successfully-converting permission flows do, versus a jarring immediate prompt on page load).

"denied" deserves particular care: most browsers won't re-show their native prompt once a user has explicitly denied a permission, so calling the requesting API again typically produces an automatic, silent denial. The correct response is guiding the user toward manually re-enabling it through the browser's own site settings UI, rather than silently failing or repeatedly attempting a request that cannot succeed.

const { state } = await navigator.permissions.query({ name: "camera" });
if (state === "granted") startCamera();
else if (state === "prompt") showWhyWeNeedCameraAccess();
else showManualSettingsInstructions();
localhost:3000
āœ“ Three States, Three Deliberately Different FlowsNever fire the same generic prompt regardless of what's already known about permission state.

3PermissionStatus Stays Live — The change Event

The object returned by query() isn't a one-time snapshot of state at the moment it was called — it remains live for as long as a reference to it is held, firing its own change event whenever the underlying permission is updated, whether from the user adjusting it through the browser's site settings UI in another tab, or an automatic system-level change. Listening for change lets a page's UI update immediately and correctly, without polling on an interval or requiring the user to reload.

This is particularly relevant for long-lived pages — a video call app open in a background tab whose camera permission gets revoked mid-session should be able to detect and react to that immediately, rather than only discovering the loss of access the next time it attempts to use the camera and receives an error.

const status = await navigator.permissions.query({ name: "microphone" });
status.addEventListener("change", () => updateUIFor(status.state));
localhost:3000
āœ“ No Polling RequiredThe PermissionStatus object notifies your code directly when its state changes externally.

4A UX Layer, Not A Security Boundary — And Coverage Varies

It's essential to understand that query() is informational only — it never replaces the actual, independent permission check the underlying API (getCurrentPosition(), getUserMedia(), the Notification constructor, etc.) performs at the moment it's actually called. State can change between a query() call and the subsequent real request, so the real API's own success/error handling must always still be implemented, even after checking a "granted" state moments earlier.

Coverage of which permission names can be queried also varies across browsers — not every permission-gated feature supports query() everywhere, and calling it with an unsupported name rejects the returned promise. Wrapping the call in error handling and falling back to simply requesting the permission directly (accepting a slightly less-optimized UX in that specific browser) is the appropriate defensive pattern.

try {
  const status = await navigator.permissions.query({ name: "geolocation" });
} catch {
  requestLocationDirectly(); // fallback where querying isn't supported
}
localhost:3000
⚠ Always Handle The Real API's Error Path Tooquery() informs UX decisions; it never substitutes for the actual API's own access enforcement.

5Step-by-Step Breakdown

Ask Before You Ask. Calling geolocation or the camera directly the moment a page loads is exactly how users learn to reflexively click 'Block' on every permission prompt. The Permissions API lets you check whether access is already granted, denied, or undecided — before you ever trigger a prompt.

query() Returns state Without Triggering A Prompt. navigator.permissions.query({ name: "geolocation" }) resolves with a PermissionStatus object whose state is "granted", "denied", or "prompt" — critically, calling query() itself never shows a permission dialog to the user; it only reports the current state.

What query() Does. Does calling navigator.permissions.query() ever trigger a browser permission prompt on its own?

  • →No — it only reports the current permission state, without any side effect
  • →Yes, every call shows a prompt if the state isn't already decided
  • →Only for camera and microphone permissions specifically

Use state To Design Better UX Before Requesting Access. Checking state first lets you skip an unnecessary prompt entirely when already "granted", show a clear explanatory UI ("Enable location to see nearby stores") before actually requesting when the state is "prompt", and offer a helpful fallback or settings link instead of a doomed repeat prompt when "denied".

Designing Around Permission State. A permission's state is "denied". What's the appropriate UX response, given that browsers won't re-prompt once explicitly denied?

  • →Show instructions for manually re-enabling it in browser settings, instead of calling the API again
  • →Call the permission-requesting API anyway, since it might succeed this time
  • →Do nothing and silently disable the feature with no explanation

PermissionStatus Is Live — Listen For change. The object returned by query() isn't a one-time snapshot: it fires a change event whenever the user updates that permission (via the browser's own site settings UI) while your page is open, letting the UI react immediately without requiring a reload or a re-query on a timer.

Reacting To Live Permission Changes. A user opens their browser's site settings in another tab and revokes camera access while your page is still open. How can your page detect this without polling?

  • →Listen for the change event on the PermissionStatus object returned by query()
  • →It's impossible without the user reloading the page
  • →Repeatedly call query() on a setInterval timer

Coverage Varies By Permission Name And Browser. Not every permission-gated API is queryable — coverage (geolocation, camera, microphone, notifications, clipboard-read, and others) varies by browser, and querying an unsupported name rejects the returned promise, so feature-detecting support before relying on it is necessary defensive practice.

Permissions API Coverage. What happens if you call query() with a permission name your browser doesn't support querying?

  • →The returned promise rejects, which should be caught
  • →It always resolves with state "prompt" as a safe default
  • →It throws synchronously before returning a promise at all

Querying Is A UX Optimization, Not A Security Boundary. The Permissions API only informs your UI decisions — the actual access-granting API (getCurrentPosition, getUserMedia, etc.) independently and authoritatively enforces the real permission check at call time regardless of what query() previously reported, so state must never be treated as a substitute for handling the real API's own success/failure outcome.

query() Is Not A Security Guarantee. If navigator.permissions.query() reports state "granted", is it safe to skip handling the error callback of the actual permission-requesting API?

  • →No — the actual API call still independently enforces the permission and can still fail
  • →Yes, a "granted" state guarantees the subsequent call will succeed
  • →It depends entirely on which specific browser is being used

Permissions API Mastered. You now know how to check a permission's state before requesting it, design distinct UX paths for granted/prompt/denied, react to live permission changes with the change event, and understand that query() informs UX without replacing the real API's own access enforcement.

Add A Permission-Request Trigger. Add a data attribute naming which permission this button will request.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1Explaining A Permission Request Before Triggering It Benefits All Users, Especially Those Using Assistive Technology

An unexpected native browser dialog can be particularly disorienting for screen reader users navigating linearly — showing an in-page explanation first, using the Permissions API's state, gives clear context before the interruption occurs.

Best Practices

Never Trigger A Permission-Requesting API On Page Load Without User-Initiated Context

Use query() to check state first, and only request access following a deliberate user action (clicking a clearly-labeled 'Enable Location' button), which also tends to produce meaningfully higher grant rates.

Always Still Handle The Real API's Own Error/Denial Path, Even After A 'Granted' Query Result

State can change between the query and the actual call, and query() itself is never the authoritative security check — only the real API is.

Frequent Bugs

THE BUG

A page repeatedly triggers a permission prompt on every load for a permission the user already explicitly denied, since the state was never checked first.

THE FIX

Query state before requesting, and for a denied state, show manual re-enable instructions instead of calling the requesting API again.

THE BUG

Code checks permissions.query() once on load and assumes that state is accurate for the entire session.

THE FIX

Attach a change listener to the returned PermissionStatus object to react to permission updates that happen while the page remains open.

Real-World Examples

A Location Feature With Deliberate, State-Aware UX

A store-locator feature that only prompts for geolocation after explaining why, and never re-prompts a user who already denied it.

const status = await navigator.permissions.query({ name: "geolocation" });

if (status.state === "granted") {
  findNearbyStores();
} else if (status.state === "prompt") {
  showEnableLocationButton(); // requests only on click
} else {
  showManualEnableInstructions();
}

status.addEventListener("change", () => location.reload());

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Triggering a permission request immediately on page load with no user-initiated context

// Wrong: fires on load navigator.geolocation.getCurrentPosition(onSuccess); // Correct: gated behind a user action, informed by query() enableLocationButton.addEventListener("click", () => navigator.geolocation.getCurrentPosition(onSuccess, onError));

The Solution //

Query state first, and only call the actual requesting API following a clear, user-initiated action.

The Error //

Treating a "granted" query() result as a guarantee, skipping the real API's error handling

navigator.geolocation.getCurrentPosition(onSuccess, onError); // onError still required

The Solution //

Always implement the actual API's error callback, since state can change or query() coverage can be imperfect.

Lesson Glossary

[01]navigator.permissions.query()

Reads a permission's current state without triggering a prompt.

Code Preview
await navigator.permissions.query({ name: "geolocation" })

[02]PermissionStatus

A live object reporting state, with a change event.

Code Preview
status.state, status.addEventListener("change", …)

[03]state (granted/denied/prompt)

The three possible permission states a query resolves to.

Code Preview
"granted" | "denied" | "prompt"

[04]change event

Fires on a PermissionStatus when the permission is updated externally.

Code Preview
status.addEventListener("change", handler)

Continue Learning