Permissions API: Querying Permissions Proactively
AI generated
JS
() =>
JavaScript · Browser APIs · UX
Permissions API
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.

15 min read navigator.permissions PermissionStatus Proactive UX

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.

11. FAQ: Permissions API: The Essentials at a Glance

1What exactly does navigator.permissions.query() do?
It queries the current status of a permission without triggering the native permission dialog, and returns granted, denied, or prompt.
2Can I grant a permission with the Permissions API?
No, the API is purely read access, the actual request still happens through the respective feature API like getUserMedia().
3What does the status prompt mean?
The user has not been asked yet, calling the feature API now would trigger the native permission dialog.
4How do I learn about later status changes?
Via the change event on the returned PermissionStatus object, which fires as soon as the status changes during the session.
5Why should you not ask for permissions immediately on load?
Because users often reflexively reject context-free dialogs, and a dialog rejected once cannot simply be triggered again in most browsers.
6Do all browsers support the same permission names?
No, the scope differs noticeably, geolocation, notifications, camera, and microphone are widely supported, more exotic names less so.
7What happens with an unknown permission name?
In some browsers the returned promise is rejected, so every call should sit inside a try-catch block with a sensible fallback.
8Is a custom explanation before the native dialog worthwhile?
Yes, a custom intermediate step with context before the dialog demonstrably increases the acceptance rate, but should only be shown on prompt status.
9Does denied behavior differ by permission?
Yes, some browsers no longer trigger a new dialog at all for permanently denied permissions, instead letting the feature API fail immediately.
10What if the browser does not support the Permissions API at all?
The most robust approach is falling back to the classic, direct request via the respective feature API without the prior status check.