JavaScript Permissions API Table
| Piece | What it does | Field note |
|---|---|---|
navigator.permissions.query({name}) | Ask permission STATE | granted / denied / prompt - without triggering the prompt |
state 'granted' | Pre-approved | Skip the explainer - go straight to the feature |
state 'prompt' | Not yet asked | THE moment for the value-first explainer before the browser ask |
state 'denied' | The stuck state | Browser will not re-prompt - UI must link to site settings |
onchange | Permission is live | User flips it in settings - your UI follows without reload |
Names vary by feature | geolocation, notifications, camera | Push has userVisibleOnly; not all names everywhere |
Query ≠ request | Read vs ask | query never prompts - only feature calls (or register) can |
Design for all three | The real deliverable | Granted/Denied/Prompt each deserve a distinct UI state |
The Permissions API reads permission STATE without triggering the prompt: navigator.permissions.query({name: 'geolocation'}) returns granted, denied or prompt - the three UI states every powerful feature must design for before its first use.
Bottom line: query never prompts (only the feature call can ask), prompt is THE moment for the value-first explainer before the browser's ask, denied is permanent-ish - the browser will not re-prompt, so the UI links to site settings - and onchange follows the user flipping it in settings live.
The honest part: the API is a status board, not a request button - names vary per feature (push adds userVisibleOnly, camera is 'camera'), and some permissions are not queryable everywhere. The real deliverable is designing distinct Granted, Denied and Prompt experiences.
How to use
- Read state before UI: const { state } = await navigator.permissions.query({ name: 'notifications' }) - granted skips explainers; denied shows the settings link.
- Design the prompt moment: state === 'prompt' is where your in-app explainer earns the browser prompt - value first, ask second, never on page load.
- Follow live changes: permission.onchange = e => updateUI(e.target.state) - the user flips it in settings; your interface follows without a reload.
Frequently asked questions
What are the three permission states and what UI does each deserve?
Granted: the feature works - show it working, no ceremony. Prompt: never asked - this is the state where your explainer lives: show what the feature delivers IN CONTEXT (a map preview for location, a sample notification), and only then trigger the actual browser ask. Denied: the hardest state - the browser will not re-prompt, so the UI must say 'notifications are blocked for this site', link to the browser's site-settings page, and offer whatever fallback exists (manual entry, email digest). Most products design only the granted path; the denied path is where trust and support tickets are won.
What is the difference between querying a permission and requesting it?
Read versus ask. permissions.query() never shows UI - it reports the current state silently, safe to call on load. The REQUEST happens through the feature itself: Notification.requestPermission(), getUserMedia(), geolocation.getCurrentPosition() - each of which may show the browser prompt. The separation matters for flow design: query on page load to decide which UI to render (feature button vs settings link vs explainer), and reserve the request for the moment of user intent. Calling the feature directly on load both surprises users with a prompt and burns the one first impression the permission gets.
Why does the permission state change outside my page, and how do I follow it?
Because permissions belong to the USER, not to your page: they can be flipped in browser settings at any moment, reset by clearing site data, or changed by policy. The onchange event on the PermissionStatus object fires when that happens - the UI updates live (a granted-then-denied camera flow switches to the blocked state without a reload). Design note: an onchange to 'denied' mid-feature needs graceful degradation (stop the stream, show the fallback), not an error - the user did it on purpose. This live-follow quality is also what makes the query API a status board rather than a one-time check.
What are the practical limits of the Permissions API?
Coverage and granularity vary. The name space is per-feature and not uniform: geolocation, notifications, camera, microphone, clipboard-read/write, midi - each with its quirks (push requires the userVisibleOnly flag and pairs with a service worker). Some permissions are not queryable on all engines, and some browsers return 'prompt' where the underlying feature would work without a prompt at all. The robust pattern: try the feature call and handle its own rejection as the source of truth, using the Permissions API to CHOOSE UI states beforehand and to follow changes - a status board and early-exit, not the gatekeeper.