Query permission status before the dialog interrupts
A permission dialog that appears without context gets reflexively rejected by many users. The Permissions API lets you query the current status of camera, location, notifications, and more in advance, and adjust your own interface accordingly before a dialog is even triggered.
Table of Contents
- 1. The problem with blindly triggered permission dialogs
- 2. navigator.permissions.query() as the solution
- 3. Tracking changes live
- 4. Which permission names are supported
- 5. A proven UX pattern: pre-explanation
- 6. The special case of push notifications
- 7. Limits of the API
- 8. Browser support and feature detection
- 9. A central utility for multiple permissions
- 10. Summary
- 11. FAQ
1. The problem with blindly triggered permission dialogs
Permission dialogs for camera, microphone, location, or notifications are powerful but also intrusive UI elements that the browser itself controls and that an application cannot visually influence. If such a dialog appears without an apparent reason right when the page loads, many users reject it reflexively, even if the feature would actually be needed later.
In most browsers, a dialog rejected once cannot simply be triggered again, the user has to manually reset the permission in the browser's site settings. This makes a thoughtless, premature request costly: the feature stays permanently blocked until the user takes action themselves, which rarely happens in practice. This finality clearly sets permission dialogs apart from other UI elements like cookie banners, where a repeated attempt usually has no lasting consequence.
2. navigator.permissions.query() as the solution
The Permissions API allows you to query the current status of a permission without triggering the user-facing dialog. The call navigator.permissions.query({ name: 'camera' }) returns a promise that resolves with a PermissionStatus object whose state property takes one of three values: granted, denied, or prompt.
With these three states, the interface can be prepared specifically: with granted, the feature can be activated directly, with denied it is better to show a hint with instructions for manual enabling instead of a pointless repeated dialog, and with prompt an explanatory step beforehand is worthwhile before the actual dialog appears.
async function checkCameraPermission() {
try {
const status = await navigator.permissions.query({ name: 'camera' });
return status.state; // 'granted' | 'denied' | 'prompt'
} catch (err) {
// Some browsers do not recognize 'camera' as a query name
return 'unsupported';
}
}
const state = await checkCameraPermission();
if (state === 'granted') {
startVideoCall();
} else if (state === 'denied') {
showManualEnableInstructions();
} else {
showExplanationBeforeRequest();
}
3. Tracking changes live
The PermissionStatus object stays active after the query and fires a change event as soon as the status changes, for example because the user manually adjusts the permission in browser settings during the session. This lets the interface update in real time without the page needing to reload.
This is particularly relevant in longer-running applications like video conferencing tools, where a user might revoke camera permission during an ongoing meeting. Without the change listener, the application would only learn about the revocation on the next active camera access, with the listener it can react immediately and inform the user.
const status = await navigator.permissions.query({ name: 'camera' });
status.addEventListener('change', () => {
console.log('New permission status:', status.state);
if (status.state !== 'granted') {
pauseVideoCallAndNotifyUser();
}
});
4. Which permission names are supported
The scope of queryable permission names differs noticeably between browsers. Widely and reliably supported are among others geolocation, notifications, camera, microphone, clipboard-read, and clipboard-write, while more exotic names like midi or persistent-storage are not available everywhere.
Since an unknown name causes a rejected promise in some browsers, every call should sit inside a try-catch block with a sensible fallback for the case where the query itself fails. The application must not block in that case, but should behave as if the status were unknown and use the regular permission flow.
5. A proven UX pattern: pre-explanation
An effective pattern combines the status query with your own, fully styleable intermediate step: before the native dialog appears, the application shows a custom, branded explanation of why the permission is needed, with a clear call-to-action button. Only a click on this button triggers the actual getUserMedia() or getCurrentPosition() call and thus the native dialog.
This detour through a custom UI feels like an extra step at first but demonstrably increases the acceptance rate, because the user understands the context before the browser dialog appears. The status query ensures this intermediate step is only shown when the status is actually prompt, not when permission has already been granted or permanently denied.
async function requestCameraWithExplanation() {
const status = await navigator.permissions.query({ name: 'camera' });
if (status.state === 'granted') {
return navigator.mediaDevices.getUserMedia({ video: true });
}
if (status.state === 'prompt') {
const confirmed = await showCustomExplanationDialog(
'We need access to your camera for the video call.'
);
if (!confirmed) return null;
}
// With 'denied', getUserMedia immediately throws an error you can catch
return navigator.mediaDevices.getUserMedia({ video: true });
}
6. The special case of push notifications
For notifications, an additional older status source, Notification.permission, exists alongside the Permissions API, which can be read synchronously, while navigator.permissions.query({ name: 'notifications' }) works asynchronously and detects finer state changes in some browsers. For simple checks the synchronous variant is often enough, for reactive UI with live updates the Permissions API is preferable.
Push notifications are also a good example of how important timing is: a request right after the first page visit tends to have significantly lower acceptance rates than a request that appears only after a meaningful user interaction, for example after the user has successfully completed a first order.
In practice, it is worth combining the notification status query with a simple internal counter that tracks how often a user has already interacted with the application before a custom explanation is even shown. This avoids confronting first-time visitors who do not yet know the site with a request that makes no sense to them content-wise at that point.
7. Limits of the API
The Permissions API itself cannot grant or revoke a permission, it is purely a read access to the current state. The actual request remains the responsibility of the respective feature API, such as getUserMedia() for camera and microphone or getCurrentPosition() for location.
Additionally, behavior for denied differs by permission: some browsers no longer trigger a new dialog at all for a permanently denied camera permission, instead letting the feature API fail immediately with an error, which is why your own error handling must be robust against both cases.
Another limit concerns granularity: the API returns a coarse status for an entire permission category but knows no finer gradations, such as those some native operating systems offer, for example a time-limited location grant only for the duration of the current session. Anyone needing this kind of finer control has to replicate it at the application level, for instance by treating granted permissions as uncertain again in their own state after a certain time and re-checking. iframe contexts also deserve special attention: embedded third-party content does not automatically inherit permissions from the parent document, which is why a status query inside an iframe must happen separately and be explicitly allowed via a matching Permissions-Policy in the parent document.
8. Browser support and feature detection
All modern Chromium-based browsers as well as Firefox broadly support navigator.permissions.query(), Safari supports a subset of permission names but not all. Before use, a simple check for 'permissions' in navigator is worthwhile, combined with the already mentioned try-catch around the actual query() call.
If support is completely missing, the application behaves most robustly by simply falling back to the classic, direct request via the respective feature API, without the prior status check. The user then sees the regular dialog without the extra pre-explanation, but loses no core functionality.
9. A central utility for multiple permissions
In larger applications with several permission-dependent features, a small central utility function is worthwhile that encapsulates status querying, error handling, and change listeners for arbitrary permission names. This keeps the call consistent everywhere in the application and the fallback logic only needs to be maintained in one place.
The table below compares typical permissions in terms of support breadth and recommended UX strategy, as practical guidance for your own implementation.
async function getPermissionState(name) {
if (!('permissions' in navigator)) return 'unsupported';
try {
const status = await navigator.permissions.query({ name });
return status.state;
} catch {
return 'unsupported';
}
}
async function watchPermission(name, onChange) {
if (!('permissions' in navigator)) return;
try {
const status = await navigator.permissions.query({ name });
onChange(status.state);
status.addEventListener('change', () => onChange(status.state));
} catch {
onChange('unsupported');
}
}
watchPermission('geolocation', (state) => {
document.querySelector('#location-hint').hidden = state === 'granted';
});
| Permission | Query name | Support breadth | Recommended UX strategy |
|---|---|---|---|
| Camera | camera | Broad in Chromium and Firefox, partial Safari | Pre-explanation on prompt, direct start on granted |
| Location | geolocation | Very broadly supported | Request contextually, never on page load |
| Notifications | notifications | Broadly supported | Request only after meaningful interaction |
| Clipboard read | clipboard-read | Broad in Chromium, limited elsewhere | Query only on active need, not preemptively |
Mironsoft
Modern browser APIs, performance, and maintainable JavaScript
JavaScript that holds up in the real browser, not just in the tutorial?
We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.
Code Review
Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.
Performance Optimization
Improving bundle size, load time, and runtime performance with modern APIs.
Modernization
Deliberately introducing native browser APIs instead of heavy libraries.
10. Summary
Permissions API: The Essentials at a Glance
Core function
navigator.permissions.query() returns granted, denied, or prompt without triggering a dialog.
Reactivity
The change event on PermissionStatus reports status changes in real time.
UX benefit
Show a custom pre-explanation only on prompt, demonstrably raises the acceptance rate.
Limit
Pure read access, the actual request still runs through the respective feature API.