Tags: web-dev concept

Permissions and User Gestures

Date: 2026-08-17


Some browser APIs only work in direct response to a real interaction, and others need explicit permission that can be permanently denied. Both exist because the alternative is every page on the web autoplaying audio, opening windows and asking for your location on load — and both mean the API can fail for reasons that have nothing to do with your code.


Transient activation

A user gesture — a click, a tap, a key press — grants a short-lived window during which privileged APIs work. Not scroll, not mouse movement, not a programmatic dispatch.

user clicks
  │
  ├── transient activation granted (~a few seconds)
  │     ↓
  │   privileged calls work here
  │     ↓
  └── expires. also CONSUMED by some APIs on first use

The window is lost across an await on a slow operation — the single most common cause of this failing in production:

// ✗ the gesture has expired by the time we call it
button.addEventListener('click', async () => {
  const data = await fetch('/api/prepare');   // 800ms
  window.open(data.url);                      // blocked as a popup
});
 
// ✓ act on the gesture first, fill in the detail after
button.addEventListener('click', async () => {
  const w = window.open('', '_blank');        // opened while activated
  const data = await fetch('/api/prepare');
  w.location = data.url;                      // navigate it afterwards
});

What requires a gesture: opening a window, entering fullscreen, requesting a Payment Request sheet, initiating a share, reading the clipboard, unmuted audio and video playback, requesting persistent storage, and — increasingly — showing a permission prompt at all.

Permissions

A separate mechanism, with a persistent answer.

state         meaning                        what to do

granted       previously allowed             use it
prompt        not yet asked                  ask — but see timing below
denied        refused, PERSISTED             ← the API will not work.
                                               there is no way to re-ask.
                                               only the user can undo it,
                                               in browser settings
// check before asking — this never triggers a prompt
const status = await navigator.permissions.query({ name: 'geolocation' });
 
if (status.state === 'denied') {
  showManualPostcodeEntry();          // don't call the API. explain the fallback
} else {
  showStoreFinderButton();            // ask only when they act on it
}

Denied is effectively permanent, and this is the fact that should shape the whole design. A prompt fired on page load is usually dismissed, and a dismissal in some browsers counts as a denial — so you have spent the one chance you get, on a visitor who had no idea why you were asking.

The pattern that works

✗  ON LOAD
     page loads → "example.com wants to know your location" → Block
     → geolocation permanently unavailable for this user

✓  IN CONTEXT, AFTER AN EXPLANATION
     user clicks "Find my nearest store"
     → your own UI: "We'll use your location to show nearby stores"
     → [Use my location]  [Enter a postcode instead]
     → only THEN call the API

The double prompt is deliberate. Your own interface asks first, costs nothing to decline, and can be shown again. The browser’s prompt is fired only for people who have already said yes to yours — so the acceptance rate is high and the permanent-denial risk is low.

Always ship the fallback. Postcode entry alongside geolocation, an on-page confirmation alongside notifications. The permission is an enhancement — Progressive Enhancement, Graceful Degradation.

Where this bites in commerce

  • Notification permission requested on load is the single most abused prompt on the web, and browsers have responded by making prompts harder to show and easier to block globally
  • Geolocation for store finders — the classic case for the in-context pattern above
  • Camera for barcode scanning or virtual try-on, which needs an explanation of why before the prompt
  • Payment Request and wallet APIs, which require a gesture — so a checkout that fetches a quote before invoking the sheet will fail — Checkout Design
  • Clipboard writes for “copy discount code”, which need the gesture and break if an await intervenes
  • Autoplaying video, which needs muted rather than a permission — Video and Media

Debugging it

The failure modes are quiet, and this is why they reach production:

  • A popup blocker doesn’t throw. window.open returns null. Check the return value
  • Permission denial doesn’t throw either in some APIs — it invokes an error callback you may not have supplied
  • It works locally and fails in production. Many of these APIs require a secure context; localhost counts as secure and a staging site on plain HTTP doesn’t — TLS and HTTPS
  • It works for you and not for users, because you granted the permission months ago and never reset it. Test in a fresh profile
  • Iframes need explicit delegation. An embedded frame has no permissions unless the parent grants them with allow="geolocation; camera" — Iframes and Sandboxing

The compliance overlap

Requesting a permission is asking for personal data. Location, camera and notification consent all sit alongside — not instead of — data protection obligations: a lawful basis, a privacy notice entry, and a retention period for whatever you collect. Browser permission is not the same as GDPR consent, and having one doesn’t supply the other — UK GDPR and PECR for Analytics, Legitimate Interest vs Consent.

Where it interacts

  • Iframes and Sandboxing — permission delegation across frame boundaries
  • Storage Partitioning — the Storage Access API is a permission in this same model, gated on a gesture
  • Progressive Enhancement — the design stance that makes a denied permission a degraded experience rather than a broken one
  • Deceptive Design — prompting on load, or making the decline path harder, is where this becomes a dark pattern