Skip to content

Device & OS Integration

Device and OS integration APIs, often called web capabilities, are the part of the platform that lets a Progressive Web App behave like an installed application: share into and out of the system share sheet, open and save real files, register as a file or URL-scheme handler, draw into the title bar, talk to USB, Bluetooth, serial and NFC hardware, keep the screen awake, take payments and sign in with passkeys. They matter because they decide what a PWA can replace. Most of them were created or accelerated by Google's Project Fugu, and their support splits sharply: a core set works in every engine, while a large and growing set ships only in Chromium-based browsers. This page explains how that landscape came to be, how to use these APIs safely with progressive enhancement, how the permission and user-activation gates work, how origin trials fit in, and where each capability stands in September 2026.

Key takeaways

  • Capabilities come in two shapes: declarative manifest members (share_target, file_handlers, protocol_handlers, launch_handler, display_override) that the browser wires into the OS at install time, and imperative JavaScript APIs (navigator.share(), showOpenFilePicker(), navigator.serial) that run in the page.
  • Project Fugu, announced in November 2018, drove most of these APIs through a six-step process that ends with an origin trial and a stable launch in Chromium. Mozilla and WebKit have publicly rated many of them negative or oppose, so "shipped in Chrome" does not mean "coming to Safari or Firefox".
  • Always detect features, never browsers, and detect at the right depth: interface present, option supported, platform integration active, policy allowed, permission granted. A method that exists can still do nothing (Badging on Android, screen.orientation.lock() on desktop Chrome).
  • Powerful APIs sit behind up to five gates: secure context, Permissions Policy, transient user activation (5 seconds in Chromium, Gecko and WebKit), a permission prompt or device chooser, and operating-system permissions.
  • Origin trials let you try unreleased Chromium APIs on production traffic, but tokens expire, usage is capped at 0.5% of Chrome page loads, and service workers need the token in an HTTP header.
  • Track status in three places at once: chromestatus.com for Chromium, the Mozilla and WebKit standards-positions repositories for the other engines, and MDN's browser-compat-data (or caniuse and webstatus.dev) for shipped versions.

What "capabilities" means for a PWA

A PWA's baseline is a web page with a manifest and a service worker. Everything in this section adds integration points on top of that baseline. It helps to sort them by where the integration happens, because that determines how you ship, test and detect them.

Layer How you use it When it takes effect Examples Needs installation?
OS registration Manifest member When the app is installed or its manifest update is applied share_target, file_handlers, protocol_handlers, shortcuts, scope_extensions Yes
App window chrome Manifest display_override + CSS/JS When the app window opens Window Controls Overlay, tabbed display Yes
Launch routing Manifest launch_handler + window.launchQueue Each time the OS or a link launches the app Files, protocol URLs, captured links Yes
System UI from the page JavaScript API with a browser-owned UI While the page runs, usually after a click Web Share, file pickers, Contact Picker, EyeDropper, payment sheet, passkey sheet No
Hardware JavaScript API with a device chooser While the page runs, after a click Web Bluetooth, WebUSB, WebHID, Web Serial, Web NFC No
System state JavaScript API, sometimes permission-gated While the page runs Screen Wake Lock, Idle Detection, Compute Pressure, Media Session, Window Management No
Background Service worker events Page closed Push, Background Sync, Periodic Sync, Background Fetch Sometimes

Background execution is covered in Background & Engagement, and storage primitives such as the origin private file system in Caching & Offline. The manifest mechanics behind the first three rows are documented in Advanced & Integration Members; this section focuses on the APIs and the runtime behavior you build around them.

The "needs installation" column is the one that surprises teams most. Declarative integrations have nothing to attach to until the browser creates an OS-level app, so they are inert in a browser tab. Several imperative APIs also behave differently when installed: Chromium grants File System Access permissions persistently for installed apps (Chrome 122 and later), Periodic Background Sync requires an installed app, and on iOS and iPadOS the Badging API and Web Push exist only for Home Screen web apps. See Installability Criteria for what it takes to become installable.

Project Fugu: goals and history

In November 2018 Google announced the Web Capabilities project, codenamed Project Fugu after the pufferfish that is a delicacy when prepared well and dangerous when not. The name is the thesis: powerful APIs are valuable only if they are designed so that a malicious site cannot abuse them. Chrome's own description calls it "an effort to close gaps in the web's capabilities enabling new classes of applications to run on the web", and the status page describes it as "a cross-company effort with the objective of making it possible for web apps to do anything iOS, Android, or desktop apps can." Microsoft and Intel engineers have authored or co-authored many of the resulting proposals alongside Google; Window Controls Overlay and manifest protocol handling, for example, started as Microsoft Edge explainers. The broader timeline is in History & Evolution.

The capabilities process

Chrome's original announcement defines six steps every capability goes through. They map directly to Chromium's Blink launch process and to the entries you see on chromestatus.com:

flowchart LR
    A["1. Identify the developer need"] --> B["2. Create an explainer"]
    B --> C["3. Get feedback, iterate"]
    C --> D["4. Move to a specification"]
    D --> E["5. Origin trial"]
    E --> F["6. Ship"]
    C -. "no support / no need" .-> X["Dropped"]
    E -. "design changes" .-> D
  1. Identify the developer need. A use case, not an API: "edit a local file in place", "configure a microcontroller over a serial port".
  2. Create an explainer. A design document in a public repository (usually under the WICG GitHub organization) describing the problem, non-goals and sample code.
  3. Get feedback and iterate on the explainer. Public discussion with web developers and other browser vendors, plus a formal request for their standards positions.
  4. Move the design to a specification and iterate. Usually a WICG Community Group draft first, sometimes later adopted by a W3C Working Group.
  5. Origin trial. The implementation ships in stable Chrome, but only for origins that register (see origin trials below).
  6. Ship it. The feature is enabled by default after an "Intent to Ship" is approved by Blink's API owners.

The Fugu status page groups proposals into five public stages: under consideration, work has begun, available behind a flag, in an origin trial and available in stable. It also states plainly that "many ideas never make it past the explainer or origin trial stage." Declarative Link Capturing, url_handlers and the original handle_links proposal are examples: each ran as an experiment and was replaced or abandoned.

What Fugu shipped, and when

The table lists representative capability launches in Chrome, with dates from Chromium's release schedule and versions from MDN's browser-compat-data. Pre-Fugu APIs that the project later extended are included for context.

Chrome Stable date Capabilities that shipped
56 January 2017 Web Bluetooth (Android and macOS first; Windows and other desktops from Chrome 70)
61 September 2017 Web Share on Android, WebUSB
66 April 2018 Async Clipboard readText() / writeText()
76 July 2019 Async Clipboard read() / write() for images; Web Share Target POST and files on Android
80 February 2020 Contact Picker (Android), Periodic Background Sync
81 April 2020 Badging API (Windows, macOS)
84 July 2020 Screen Wake Lock
86 October 2020 File System Access (desktop), origin private file system
89 March 2021 Web Serial, WebHID, Web NFC (Android), Web Share on Windows and ChromeOS, Web Share Target on ChromeOS
94 September 2021 Idle Detection, VirtualKeyboard API
95 October 2021 EyeDropper
96 November 2021 Manifest protocol_handlers, manifest id
100 March 2022 Window Management (multi-screen placement)
102 May 2022 File Handling, window.launchQueue
103 June 2022 Local Font Access
105 August 2022 Window Controls Overlay
110 February 2023 launch_handler, LaunchParams.targetURL
116 August 2023 Document Picture-in-Picture
122 February 2024 Persistent File System Access permissions for installed apps
125 May 2024 Compute Pressure
128 August 2024 Web Share on macOS
132 January 2025 File System Access pickers on Android
133 February 2025 FileSystemObserver
138 June 2025 Web Serial over Bluetooth RFCOMM on Android
139 August 2025 scope_extensions (desktop; MDN's data lists 138)
141 September 2025 Digital Credentials API
144 January 2026 <geolocation> element
148 May 2026 Web Serial with wired ports on Android

The Fugu API tracker (fugu-tracker.web.app) now simply redirects to a saved search in the Chromium issue tracker filtered on the proj-fugu label, and the Chrome team maintains a Fugu showcase of production apps that use the APIs.

The fault line Fugu exposed

Fugu's design goals were security, privacy and user trust: permissions the user controls and can revoke, transparency when an API is in use, and choosers instead of blanket access. Mozilla and Apple accept that framing for some APIs and reject it for others. Their core objections recur across their positions:

  • Consent is not meaningful for low-level access. A user cannot evaluate whether granting a site access to a USB device, a serial port or a folder is safe. Mozilla's File System Access position argues that meaningful consent for cross-site access to the local file system is not possible.
  • Devices are not built for hostile input. Firmware on USB, HID and Bluetooth peripherals assumes a trusted host driver. A web page is not one.
  • Fingerprinting surface. Fonts, screens, idle state, sensors and connected devices all add entropy. WebKit's tracking-prevention documentation lists APIs it has "decided to not yet implement due to fingerprinting, security, and other concerns", including Web Bluetooth, Web MIDI, Magnetometer, Web NFC, WebHID, Serial, WebUSB and User Idle Detection.

The result is a web with three tiers, described next. Because every browser on iOS and iPadOS uses WebKit, the WebKit column of any support table is also the iPhone column for Chrome, Edge and Firefox on those devices; see iOS & iPadOS.

Standardized across engines vs Chromium-only

"Is it standardized?" is the wrong question. Almost every Fugu API has a specification, usually a WICG Community Group report, and several have W3C Working Group drafts. The useful question is how many engines implement it, because that determines whether you can build on it or only enhance with it.

Tier 1: works in every engine

These are safe foundations. You still feature-detect, because versions and platform details differ, but the fallback path is for old browsers, not for a whole engine.

  • Web Share (navigator.share()), everywhere except Chrome on Linux, Firefox desktop and Android WebView: see Web Share API.
  • Screen Wake Lock: Chrome 84, Firefox 126, Safari 16.4 (inside iOS Home Screen web apps only from iOS 18.4).
  • Async Clipboard read and write: Chrome 66/76, Firefox 125/127, Safari 13.1.
  • Media Session: Chrome 73 desktop, Firefox 82, Safari 15.
  • Web Push and Notifications: covered in Push Notifications and Web Push on iOS & Safari.
  • Web Authentication and passkeys, Payment Request (Safari supports it with Apple Pay; Firefox only behind a flag): see Authentication & Passkeys and Payments.
  • Origin private file system, Web Locks, Storage persistence, Geolocation, Gamepad, Permissions API query().

Tier 2: Chromium plus one other engine

These have a real second implementation, which makes them more durable, but still leave a large population without support.

  • Badging API: Chromium desktop and Safari (iOS/iPadOS 16.4 Home Screen web apps, macOS Sonoma web apps). Firefox has no implementation. See Badging API.
  • Web MIDI: Chromium and Firefox 108 (gated by a site-permission add-on).
  • Web Serial: Chromium desktop and Firefox 151 on desktop (also add-on gated). Safari opposes it.
  • Document Picture-in-Picture: Chrome 116 desktop and Firefox 151 desktop.
  • Digital Credentials API: Chrome 141 and Safari 26.
  • registerProtocolHandler(): Chromium desktop and Firefox, though only Chromium supports the manifest protocol_handlers member.

Tier 3: Chromium-only

Everything else in this section ships only in Chromium-based browsers, and in many cases only on some Chromium platforms: File System Access pickers, File Handling, manifest protocol_handlers, launch_handler, Window Controls Overlay, Web Share Target (Android and ChromeOS), WebUSB, WebHID, Web Bluetooth, Web NFC, Idle Detection, Local Font Access, Window Management, EyeDropper, Compute Pressure, Contact Picker, Keyboard Lock, Background Sync, Periodic Background Sync, Background Fetch, Payment Handler, FedCM and getInstalledRelatedApps().

"Chromium-only" hides further variation that you must test:

  • Platform gaps inside Chromium. WebHID, EyeDropper, Local Font Access, Window Controls Overlay and File Handling are desktop-only. Web NFC and the Contact Picker are Android-only. Web Share is absent from Chrome on Linux. EyeDropper is unavailable on Linux Wayland.
  • Android WebView. Most capability APIs are missing from WebView (Web Share, Web Bluetooth, WebUSB, Push, Background Sync), so the same site inside a native app's WebView loses them. Trusted Web Activities run in the full browser and keep them; see Trusted Web Activity.
  • Other Chromium browsers. Edge and Opera track Chromium versions closely. Samsung Internet lags and sometimes differs (it ships Web Share Target, but its partial Contact Picker support was removed in version 22). Brave ships some APIs, such as File System Access, only behind a flag. Vendors can also disable a feature through server-side configuration.

Experimental

Some capabilities in this section are not enabled by default in any stable browser in September 2026. The Web Install API is the most visible example: navigator.install() ran an origin trial on desktop in Chrome 143 to 148, extended through 150, the declarative <install> element ran its own trial in Chrome and Edge 148 to 153 (now ended), and both sit behind a flag in Chrome 154. An Intent to Ship posted in September 2026 targets desktop Chrome 156 for navigator.install() (and, per Chrome Platform Status, <install>); Android is not part of it. WebKit opposes the API and Gecko has no recorded signal. Further browser-rendered capability elements beyond <geolocation> are still moving through Chromium's launch process. Treat them as previews and never make a core flow depend on them.

Where the other engines stand

The standards-positions repositories are the authoritative record. The table summarizes the recorded positions for capabilities discussed in this section as of September 2026. "No position" means an issue exists or the API was never formally reviewed, without a recorded verdict.

Capability Mozilla position WebKit position
Screen Wake Lock Positive (shipped) Support (shipped)
Badging API Positive Shipped
Web Share Target Positive Neutral
Clipboard API Positive (shipped) Shipped
Permissions API Positive (shipped) Shipped
File System Access Negative Oppose
File Handling Defer No position
Manifest protocol handlers Defer No position
Window Controls Overlay Defer No position
Web App Launch Handling No position No position
Contact Picker Defer No position (implemented behind a flag)
Web Bluetooth Negative Oppose
WebUSB Negative Oppose
WebHID Negative No position (on WebKit's "not implementing" list)
Web Serial Neutral (shipped in Firefox 151) Oppose
Web NFC Negative Oppose
Generic Sensors Negative No issue for the core spec; opposes Geolocation Sensor
Idle Detection Negative No position (on WebKit's "not implementing" list)
Keyboard Lock Negative No position
Compute Pressure No position Oppose
Local Font Access No position No position
Window Management No position No position
EyeDropper No position No position
Periodic Background Sync Negative No position
Web Background Sync Negative No position
Payment Handler Defer No position
Get Installed Related Apps Negative No position
Digital Credentials Negative Support (shipped in Safari 26)
Page Embedded Permission Control Negative Oppose
<geolocation> element Positive No position
Web Install API No position Oppose
Isolated Web Apps Negative No position (issue labeled "blocked")

Sources: Mozilla standards positions and WebKit standards positions. Positions change; always follow the linked issue before making a roadmap decision. Isolated Web Apps have their own page because they unlock APIs that are not available to ordinary PWAs at all.

Progressive enhancement and feature detection

Capabilities are enhancements by definition. A photo editor that can save back to the original file with File System Access still has to download a copy in Safari; a hardware configurator that uses Web Serial still has to explain what to do in Firefox for Android. The engineering pattern is always the same: baseline that works everywhere, enhanced path when the capability is present, and UI that only offers what will work.

Detect at the right depth

Presence of an interface is only the first of several questions. Each layer can fail independently:

Depth Question How to test Example of a false positive at the previous depth
1. Interface Does the browser expose the API? "share" in navigator
2. Input Does it accept this input or option? navigator.canShare({ files }), try/catch around option use Firefox for Android has navigator.share but canShare({ files }) returns false
3. Platform integration Does calling it have an effect here? Platform knowledge, display-mode media queries navigator.setAppBadge() resolves on Chrome for Android and Linux but shows nothing
4. Policy Is this document allowed to use it? document.featurePolicy?.allowsFeature() (Chromium), or catch NotAllowedError/SecurityError A cross-origin iframe without allow="web-share"
5. Permission Has the user granted it? navigator.permissions.query() Contact Picker exists, user declines

Two more rules keep detection honest:

  • Detect with the global object that owns the API. "showOpenFilePicker" in window, "serial" in navigator, "launchQueue" in window, "BarcodeDetector" in globalThis. Checking typeof Serial works too but is easier to shadow by accident.
  • Never infer one capability from another, or from the user agent. OPFS support says nothing about pickers; Web Share on macOS Safari says nothing about Web Share Target. User-agent sniffing breaks every time a browser adds support, and Chromium's User-Agent Client Hints (navigator.userAgentData) are not available in Firefox or Safari anyway.

A capability detection module

Keep detection in one module so that UI code asks a named question and the answer can be logged, tested and overridden. Every check below is synchronous and cheap; nothing prompts the user.

src/capabilities.js
// Synchronous, side-effect-free feature detection for capability APIs.
// Answers "does this browser expose the API?", never "will the user allow it?".

const nav = globalThis.navigator ?? {};
const win = globalThis;

function canShareFiles() {
  // canShare() must exist AND accept a file: Firefox for Android exposes
  // canShare() but returns false for files.
  if (typeof nav.canShare !== "function") return false;
  try {
    const probe = new File(["probe"], "probe.txt", { type: "text/plain" });
    return nav.canShare({ files: [probe] });
  } catch {
    return false;
  }
}

function matchesDisplayMode(...modes) {
  return modes.some((mode) => win.matchMedia?.(`(display-mode: ${mode})`).matches);
}

export const capabilities = Object.freeze({
  // Tier 1: cross-engine
  share: typeof nav.share === "function",
  shareFiles: canShareFiles(),
  wakeLock: "wakeLock" in nav,
  clipboardWrite: typeof nav.clipboard?.write === "function",
  mediaSession: "mediaSession" in nav,
  opfs: typeof nav.storage?.getDirectory === "function",
  webAuthn: typeof win.PublicKeyCredential === "function",
  paymentRequest: typeof win.PaymentRequest === "function",
  userActivation: "userActivation" in nav,
  permissionsQuery: typeof nav.permissions?.query === "function",

  // Tier 2: Chromium + one other engine
  badging: typeof nav.setAppBadge === "function",
  midi: typeof nav.requestMIDIAccess === "function",
  serial: "serial" in nav,
  documentPiP: "documentPictureInPicture" in win,

  // Tier 3: Chromium-only
  fileSystemAccess: typeof win.showOpenFilePicker === "function",
  fileHandling: "launchQueue" in win && "files" in (win.LaunchParams?.prototype ?? {}),
  windowControlsOverlay: "windowControlsOverlay" in nav,
  usb: "usb" in nav,
  hid: "hid" in nav,
  bluetooth: "bluetooth" in nav,
  nfc: "NDEFReader" in win,
  contactPicker: "contacts" in nav && "ContactsManager" in win,
  idleDetection: "IdleDetector" in win,
  localFonts: typeof win.queryLocalFonts === "function",
  windowManagement: typeof win.getScreenDetails === "function",
  eyeDropper: "EyeDropper" in win,
  computePressure: "PressureObserver" in win,

  // Context, not capability: are we running in an installed app window?
  installedWindow: matchesDisplayMode("standalone", "minimal-ui", "window-controls-overlay", "fullscreen"),
});

// Development aid: override detection from the console to test fallbacks,
// e.g. localStorage.setItem("cap:off", "fileSystemAccess,share").
export function isEnabled(name) {
  let disabled = [];
  try {
    disabled = (localStorage.getItem("cap:off") ?? "").split(",");
  } catch {
    // Storage can throw in some privacy modes; detection still works.
  }
  return Boolean(capabilities[name]) && !disabled.includes(name);
}

installedWindow is a context signal, not a capability: it tells you that declarative integrations may be active, not that they are. Detecting Installed Apps covers the reliable techniques.

Enhancing the UI without flashes or dead buttons

Render the baseline control in HTML and upgrade it when the capability is present. Hiding enhanced controls by default avoids a flash of a button that does nothing, and loading the enhanced code with import() keeps it out of the bundle for browsers that cannot use it.

src/save-button.js
import { isEnabled } from "./capabilities.js";

// Baseline: <a download> works in every browser.
// Enhanced: save back to the file the user opened, where File System Access exists.
export async function setUpSave(button, getDocumentBlob) {
  if (!isEnabled("fileSystemAccess")) {
    button.addEventListener("click", () => downloadCopy(getDocumentBlob()));
    return;
  }

  // Only browsers that can use it download the enhanced module.
  const { saveWithPicker } = await import("./fs-save.js");
  button.textContent = "Save";
  button.addEventListener("click", async () => {
    try {
      await saveWithPicker(getDocumentBlob()); // must run inside the click (user activation)
    } catch (err) {
      if (err.name === "AbortError") return; // user closed the picker: not an error
      if (err.name === "NotAllowedError" || err.name === "SecurityError") {
        // Blocked by policy, permission or a missing gesture: fall back rather than fail.
        downloadCopy(getDocumentBlob());
        return;
      }
      throw err;
    }
  });
}

function downloadCopy(blob) {
  const url = URL.createObjectURL(blob);
  const a = Object.assign(document.createElement("a"), { href: url, download: "document.txt" });
  document.body.append(a);
  a.click();
  a.remove();
  // Revoke after the navigation has started; revoking synchronously can cancel it.
  setTimeout(() => URL.revokeObjectURL(url), 30_000);
}

The same pattern appears throughout this section: File System Access falls back to <input type="file"> and downloads, Web Share falls back to copying a link, Hardware & Device APIs fall back to explaining the native tool the user needs.

Error names are part of the contract

Capability APIs use DOMException names consistently enough that one handler can classify most failures:

err.name Typical meaning Right response
AbortError User dismissed a picker, chooser or share sheet Do nothing; never show an error
NotAllowedError No transient activation, permission denied, Permissions Policy blocked, or document not focused/visible Offer the fallback; explain only if the user explicitly asked for this action
SecurityError Not a secure context, cross-origin frame restriction, or blocked location (for example a system folder) Fallback; log for diagnosis
NotFoundError No device matched a chooser filter, or the user cancelled a device chooser Show pairing or connection help
NotSupportedError The API exists but not for this input or platform (for example screen.orientation.lock() on desktop Chrome) Treat as unsupported
InvalidStateError Called in the wrong state (port already open, document not fully active) Fix the calling code
TypeError Invalid arguments, including unknown permission names passed to permissions.query() Fix the calling code or treat as unsupported

The permission model

A capability call passes through a series of gates before anything reaches the user or the device. Knowing which gate rejected a call is most of debugging.

flowchart TD
    A["Page calls a powerful API"] --> B{"Secure context?"}
    B -->|No| X1["API missing or SecurityError"]
    B -->|Yes| C{"Permissions Policy allows it in this frame?"}
    C -->|No| X2["NotAllowedError / SecurityError"]
    C -->|Yes| D{"Transient user activation required?"}
    D -->|"Yes, but none"| X3["NotAllowedError"]
    D -->|"Yes and present, or not required"| E{"Stored permission state"}
    E -->|denied| X4["NotAllowedError, no prompt"]
    E -->|granted| G["API proceeds"]
    E -->|prompt| F["Browser prompt or chooser"]
    F -->|"User declines / cancels"| X5["NotAllowedError or AbortError"]
    F -->|"User allows / picks"| H{"OS-level permission?"}
    H -->|Blocked| X6["Error or empty result"]
    H -->|Allowed| G

Gate 1: secure context

Every capability API in this section requires a secure context: HTTPS, or http://localhost and loopback addresses during development. In an insecure context most of them are simply not exposed (navigator.serial is undefined), so feature detection returns false rather than throwing. Check window.isSecureContext when detection unexpectedly fails on a staging server.

Gate 2: Permissions Policy

Permissions Policy (formerly Feature Policy) lets a document restrict which powerful features it and its iframes may use. Each feature has a token and a default allowlist, which for nearly all capabilities is self: the top-level document and same-origin frames may use it, cross-origin iframes may not unless delegated.

HTTP response header
Permissions-Policy: serial=(self), usb=(), hid=(), bluetooth=(), web-share=(self "https://embed.example.com"), screen-wake-lock=(self)
Delegating to an embedded frame
<iframe src="https://embed.example.com/player" allow="web-share; screen-wake-lock; clipboard-write"></iframe>

Capability-related tokens recognized by Chromium include bluetooth, camera, clipboard-read, clipboard-write, compute-pressure, display-capture, fullscreen, gamepad, geolocation, hid, identity-credentials-get, idle-detection, local-fonts, microphone, midi, payment, picture-in-picture, publickey-credentials-create, publickey-credentials-get, screen-wake-lock, serial, usb, web-share, window-management and xr-spatial-tracking, plus web-app-installation behind a flag. The authoritative list is the Permissions Policy features registry.

Cross-browser behavior differs in an important way: MDN's compatibility data lists the Permissions-Policy header as Chromium-only, while the iframe allow attribute works in all engines (Firefox 74, Safari 11.1). Setting a restrictive header is therefore defense in depth for Chromium users, not a cross-browser control. Only Chromium exposes a script API to test the policy, document.featurePolicy.allowsFeature("serial"). Permissions and Content Security Policy cover policy design for production.

Gate 3: prompts, choosers and pickers

Capabilities obtain consent in three different ways, and they persist differently:

Consent model Examples What is granted Persistence
Permission prompt Geolocation, notifications, camera, Idle Detection, Local Font Access, Window Management, clipboard read (Chromium) Access to a whole feature for the origin Until revoked; Chromium also offers "Allow this time" and removes permissions from sites you have not used recently
Device chooser Web Bluetooth, WebUSB, WebHID, Web Serial Access to the specific device the user picked Per device; USB, HID and serial grants return through getDevices() / getPorts()
Picker / sheet (implicit grant) File pickers, share sheet, Contact Picker, EyeDropper, payment sheet, passkey sheet Only the data the user selected, once None; the selection is the consent. File handles can be stored and re-permissioned

Pickers are the model Mozilla and Apple are most comfortable with, which is why Web Share, Payment Request and WebAuthn are cross-engine while device choosers are not.

Chromium's help center describes both lifetime controls: "Allow this time" means the site "will be able to use the requested feature only during your current visit", and "To protect your data, Chrome removes permissions from sites you haven't used recently." Design for permissions that disappear. Query the state at launch, not once at install.

Querying permission state

The Permissions API reports "granted", "denied" or "prompt" for a named permission. It is supported everywhere (query() since Chrome 43, Firefox 46, Safari 16), but the set of names varies by browser, and the specification requires a rejected promise with a TypeError when the name is not supported. Treat that rejection as "unknown", not as "denied".

src/permissions.js
// Returns "granted" | "denied" | "prompt" | "unsupported".
export async function permissionState(name) {
  if (typeof navigator.permissions?.query !== "function") return "unsupported";
  try {
    const status = await navigator.permissions.query({ name });
    return status.state;
  } catch (err) {
    // TypeError: this browser does not recognize the permission name
    // (e.g. "local-fonts" outside Chromium, "clipboard-read" in Firefox and Safari).
    if (err instanceof TypeError) return "unsupported";
    throw err;
  }
}

// Keep UI in sync when the user changes the permission in site settings.
export async function watchPermission(name, onChange) {
  if (typeof navigator.permissions?.query !== "function") return () => {};
  let status;
  try {
    status = await navigator.permissions.query({ name });
  } catch {
    return () => {};
  }
  const handler = () => onChange(status.state);
  status.addEventListener("change", handler);
  onChange(status.state);
  return () => status.removeEventListener("change", handler);
}
Permission name Chromium Firefox Safari
geolocation ✅ 43 ✅ 46 ✅ 16
notifications ✅ 43 ✅ 46 ✅ 16.4
push ✅ 43 ✅ 46 ✅ 17
camera, microphone ✅ 64 ✅ 132 ✅ 16
screen-wake-lock ✅ 84 ✅ 126 ✅ 16.4
persistent-storage ✅ 71 ✅ 53 ❌
midi ✅ 43 ✅ 110 ❌
clipboard-read, clipboard-write ✅ 64 ❌ ❌
local-fonts ✅ 103 ❌ ❌
window-management ✅ 111 ❌ ❌
background-sync / periodic-background-sync ✅ 62 / 80 ❌ ❌
storage-access ✅ 119 ✅ 117 ✅ 26.2

Support data as of September 2026, from MDN's browser compatibility data. Safari's change event on PermissionStatus is marked partial. navigator.permissions.request() and revoke() are not shipped anywhere by default; request permissions through each API's own method.

Page-embedded permission controls

Chromium has been moving the moment of consent into the page with browser-rendered elements. The user's click on a browser-controlled element is an unambiguous signal of intent, and it gives users who previously denied a permission a way to recover without digging into settings. The <geolocation> element shipped in Chrome 144 (January 2026) on desktop and Android after an origin trial of the more general Page Embedded Permission Control. Mozilla rates the general PEPC proposal negative but the <geolocation> element positive; WebKit opposes PEPC. Use the element as an enhancement over a regular button that calls navigator.geolocation.

User activation: transient and sticky

Most capabilities that open browser UI require transient user activation: the call must happen shortly after a genuine user interaction. The HTML Standard defines two states per window:

Sticky activation
Set the first time the user interacts with the page and never reset for the life of the document. Gates features that must not fire on load: autoplay with sound, navigator.vibrate(), the beforeunload confirmation dialog, VirtualKeyboard.show().
Transient activation
Set on each interaction and valid for a limited duration, and it can be consumed. Gates everything that opens a window, picker, chooser or sheet.

Activation is triggered only by trusted events of these types: keydown (except Escape, browser-reserved shortcuts and keys such as Caps Lock), mousedown, pointerdown with a mouse pointer, pointerup with a non-mouse pointer, and touchend. Note that click is not in the list; it is the preceding mousedown/pointerup that activates, which is why a click handler has activation. Synthetic events from dispatchEvent() never activate. Scrolling and hovering never activate.

How long activation lasts

The specification leaves the duration to implementations ("should be at most a few seconds"). All three engines use 5 seconds: Chromium's kActivationLifespan is base::Seconds(5), Firefox's dom.user_activation.transient.timeout preference defaults to 5000 milliseconds, and WebKit's defaultTransientActivationDuration is 5_s. Any await between the click and the capability call spends that budget.

Consumption and propagation

Some APIs consume activation when they succeed: window.open(), navigator.share(), PaymentRequest.show() and fullscreen requests, among others. Consumption clears transient activation in every window in the frame tree, which prevents a single click from opening a popup and a share sheet, or from being replayed by an iframe. Activation propagates upward: a click in an iframe activates its ancestors, and a click in a document also activates same-origin descendant frames. Service workers have an analogous rule: clients.openWindow() and WindowClient.focus() are allowed only for a short time after the user clicks a notification, while the notificationclick event is being handled.

APIs gated by user activation

Transient activation required Sticky activation required
navigator.share(), file pickers (showOpenFilePicker(), showSaveFilePicker(), showDirectoryPicker()), navigator.clipboard.read()/write() (browser-dependent), requestDevice() / requestPort() for USB, HID and serial, EyeDropper.open(), ContactsManager.select(), IdleDetector.requestPermission(), queryLocalFonts(), getScreenDetails(), PaymentRequest.show(), getDisplayMedia(), requestFullscreen(), documentPictureInPicture.requestWindow(), navigator.keyboard.lock(), XRSystem.requestSession(), window.open() navigator.vibrate(), VirtualKeyboard.show(), audio autoplay, beforeunload prompts

The list is based on MDN's features gated by user activation, which is non-exhaustive; each API page in this section notes its own requirement.

Checking activation and keeping it

navigator.userActivation exposes both states (isActive for transient, hasBeenActive for sticky) in Chrome 72, Firefox 120 and Safari 16.4. Use it to choose a path, not as a security check.

The classic bug is fetching data after the click and then calling the API:

src/share-report.js
// Broken: on a slow network the fetch takes longer than 5 seconds, activation
// expires, and navigator.share() rejects with NotAllowedError.
shareButton.addEventListener("click", async () => {
  const blob = await (await fetch("/reports/latest.pdf")).blob();
  await navigator.share({ files: [new File([blob], "report.pdf", { type: "application/pdf" })] });
});

The fix is to prepare the payload before the gesture, and to recover gracefully when you could not:

src/share-report.js
let prepared = null;

// Start the download when the share UI becomes visible, not when the user clicks.
export function prepareReport() {
  prepared ??= fetch("/reports/latest.pdf")
    .then((res) => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.blob();
    })
    .then((blob) => new File([blob], "report.pdf", { type: "application/pdf" }))
    .catch((err) => {
      prepared = null; // allow a retry
      throw err;
    });
  return prepared;
}

// Wire it once: shareButton.addEventListener("click", () => onShareClick(shareButton));
export async function onShareClick(button) {
  const file = await prepareReport(); // usually already resolved: no activation spent

  if (navigator.userActivation && !navigator.userActivation.isActive) {
    // Too slow: activation expired while waiting. Ask for a fresh click. The next
    // click runs this same handler, finds the file already prepared and shares at once.
    // (Do not add a second listener here: both would run and the second share()
    // would reject because the first one consumed the activation.)
    button.textContent = "Ready: tap to share";
    return;
  }
  await shareFile(file);
}

async function shareFile(file) {
  if (!navigator.canShare?.({ files: [file] })) {
    location.assign(URL.createObjectURL(file)); // fallback: open the PDF
    return;
  }
  try {
    await navigator.share({ files: [file], title: "Latest report" });
  } catch (err) {
    if (err.name !== "AbortError") throw err; // AbortError = user closed the sheet
  }
}

Origin trials: trying APIs before they ship

An origin trial enables an experimental feature in stable Chrome for origins that opt in with a token, so you can test it with real users before it is standardized. Most Fugu APIs spent months in one.

How Chrome origin trials work

  1. Pick a trial on the Chrome origin trials site and register your origin. You can optionally match all subdomains; tokens are not issued for origins on the Public Suffix List.
  2. Deliver the token with the page, in any of three ways: a <meta http-equiv="origin-trial" content="TOKEN"> element, an Origin-Trial: TOKEN response header, or a <meta> element that script inserts before the feature is used.
  3. Service workers and shared workers need the header. Dedicated workers inherit from their parent document, but a service worker is not a document, so the token must be in the Origin-Trial header of the service worker script response.
  4. Third-party trials let an embed provider enable a feature on every site that includes its script. The token must come from an external script served by the registered origin; it does not work in a <meta> tag, an inline script or a header.
  5. Tokens are verified on the device without network access, and each token carries its own expiry. Renew it when the trial is extended.

Chromium's launch process bounds the experiment. An initial origin trial may run for at most 12 milestones, and each extension may add 6 milestones at a time only with demonstrated progress on the specification, TAG review, other vendors' signals and web-platform tests. Trials are "gapless": they normally end one milestone after the version that enables the feature by default, so a successful API keeps working through the switch. A safeguard automatically disables a trial globally if usage exceeds 0.5% of all Chrome page loads, and some trials also exclude a small subset of users even when the page has a valid token.

src/origin-trial.js
// Inject an origin-trial token at run time, e.g. only for users in an experiment cohort.
// Must run before the code that uses the feature.
export function enableOriginTrial(token) {
  const meta = document.createElement("meta");
  meta.httpEquiv = "origin-trial";
  meta.content = token;
  document.head.append(meta);
}

// Feature detection is still the source of truth: the token can be expired,
// the trial can be disabled, or the user can be in the excluded subset.
nginx.conf
location = /sw.js {
    # Service workers only see origin-trial tokens delivered in the header.
    add_header Origin-Trial "YOUR_TOKEN" always;
    add_header Cache-Control "no-cache" always;
}
server.js
import express from "express";

const app = express();
const ORIGIN_TRIAL_TOKEN = process.env.ORIGIN_TRIAL_TOKEN;

app.get("/sw.js", (req, res, next) => {
  // Documents can use <meta>, but the service worker needs the header.
  if (ORIGIN_TRIAL_TOKEN) res.set("Origin-Trial", ORIGIN_TRIAL_TOKEN);
  // serve-static only adds its own Cache-Control when none is set, so this one is kept.
  res.set("Cache-Control", "no-cache");
  next();
});

app.use(express.static("dist"));
app.listen(8080);

Check the result in Chrome DevTools under Application > Frames > (frame) > Origin trials, which shows each token's status (Success, Expired, WrongOrigin and others), expiry and matching options. For local development, enable the feature in chrome://flags instead of using a token.

Other vendors and deprecation trials

Microsoft Edge runs its own origin trials program for Edge-originated features, with the same token mechanism. Firefox runs origin trials too: you request a token through a pre-filled Bugzilla template and deliver it with an Origin-Trial header or <meta> tag. WebKit does not run a public origin-trial program comparable to Chrome's; experimental WebKit features are tried in Safari Technology Preview or through Safari's feature-flag settings. Chrome also runs deprecation trials, which temporarily re-enable a removed feature for sites that need migration time.

Origin trials are not a shipping strategy

An origin-trial feature can change shape between versions, can be switched off globally if it becomes popular, and disappears when the trial ends if the feature does not ship. Put it behind feature detection with a working fallback, watch the trial's feedback and expiry emails, and do not let a core user journey depend on it.

Capability support matrix

The matrix covers the capabilities documented in this section and in Background & Engagement. Numbers are the first version with default-on support; "Chromium desktop" is Chrome and Edge on Windows, macOS and Linux unless noted.

Capability Chromium desktop Chrome Android Firefox Safari macOS Safari iOS/iPadOS Details
Web Share share() ⚠️ 89 (Win, ChromeOS), 128 (macOS), none on Linux ✅ 61 ⚠️ Android 79; desktop 🧪 ✅ 12.1 ✅ 12.2 Web Share
Web Share Target ⚠️ ChromeOS 89 only in Chrome; Edge on Windows per Microsoft ✅ 71 GET, 76 POST/files (WebAPK) ❌ ❌ ❌ Share Target
File System Access pickers ✅ 86 ✅ 132 ❌ ❌ ❌ File System Access
Origin private file system ✅ 86 ✅ 109 ✅ 111 ✅ 15.2 ✅ 15.2 OPFS
File Handling (file_handlers) ✅ 102 ❌ ❌ ❌ ❌ File Handling
registerProtocolHandler() ✅ 13 ❌ ✅ 2 ❌ ❌ Protocol Handlers
Manifest protocol_handlers ✅ 96 ❌ ❌ ❌ ❌ Protocol Handlers
launch_handler / launchQueue ✅ 110 / 102 ⚠️ 110 ❌ ❌ ❌ Launch Handling
Window Controls Overlay ✅ 105 ❌ ❌ ❌ ❌ WCO
App shortcuts ✅ 96 ✅ 84 ❌ ✅ 17.4 ❌ Shortcuts
Badging API ⚠️ 81 (not Linux) ❌ (resolves, no effect) ❌ ✅ 17 (web apps) ✅ 16.4 (Home Screen) Badging
Push API ✅ 42 ✅ 42 ✅ 44 ✅ 16.12 ⚠️ 16.4 (Home Screen) Push
Background Sync ✅ 49 ✅ 49 ❌ ❌ ❌ Background Sync
Periodic Background Sync ✅ 80 ✅ 80 ❌ ❌ ❌ Periodic Sync
Background Fetch ✅ 74 ✅ 74 ❌ ❌ ❌ Background Fetch
Screen Wake Lock ✅ 84 ✅ 84 ✅ 126 ✅ 16.4 ⚠️ 16.4 (tabs), 18.4 (Home Screen)1 Media & System
Async Clipboard (text / rich) ✅ 66 / 76 ✅ 66 / 76 ✅ 125 / 127 ✅ 13.1 ✅ 13.4 Media & System
Media Session ✅ 73 ✅ 57 ✅ 82 ✅ 15 ✅ 15 Media & System
Document Picture-in-Picture ✅ 116 ❌ ✅ 151 (desktop) ❌ ❌ Media & System
Contact Picker ❌ ✅ 80 ❌ ❌ 🧪 flag Media & System
Idle Detection ✅ 94 ✅ 94 ❌ ❌ ❌ Media & System
Local Font Access ✅ 103 ❌ ❌ ❌ ❌ Media & System
Window Management ✅ 100 ⚠️ 100 (exposed; multi-screen use is a desktop scenario) ❌ ❌ ❌ Media & System
EyeDropper ⚠️ 95 (not Linux Wayland) ❌ ❌ ❌ ❌ Media & System
Compute Pressure ✅ 125 ❌ ❌ ❌ ❌ Media & System
Screen Orientation lock() ❌ (throws) ✅ 38 ✅ 144 ❌ ❌ Media & System
Web Bluetooth ⚠️ 70 (Linux off by default) ✅ 56 ❌ ❌ ❌ Device APIs
WebUSB ✅ 61 ✅ 61 ❌ ❌ ❌ Device APIs
WebHID ✅ 89 ❌ ❌ ❌ ❌ Device APIs
Web Serial ✅ 89 ⚠️ 138 (Bluetooth), 148 (all ports) ⚠️ 151 (desktop, add-on) ❌ ❌ Device APIs
Web NFC ❌ ✅ 89 ❌ ❌ ❌ Device APIs
Web MIDI ✅ 43 ✅ 43 ⚠️ 108 (desktop, add-on) ❌ ❌ Device APIs
Payment Request ✅ 60 ✅ 53 🧪 flag ✅ 11.1 (Apple Pay) ✅ 11.3 (Apple Pay) Payments
Payment Handler ✅ 70 ✅ 70 ❌ ❌ ❌ Payments
WebAuthn / passkeys ✅ 67 ✅ 70 ✅ 60 ✅ 13 ✅ 13 Authentication
FedCM ✅ 108 ✅ 108 ❌ ❌ ❌ Authentication
Digital Credentials ✅ 141 ✅ 141 ❌ ✅ 26 ✅ 26 Authentication
getInstalledRelatedApps() ✅ 140 (installed web apps); 85 (Windows apps) ✅ 80 (Play apps), 84 (web apps) ❌ ❌ ❌ Detecting Installed Apps
Web Install API 🧪 flag in 154; intent to ship for 156 ❌ ❌ ❌ ❌ Install Prompts

Support data as of September 2026. Versions come from MDN's browser-compat-data (release 8.1.3), chromestatus.com and the individual pages linked in the last column, which carry footnotes for each ⚠️. For live data use caniuse and the "Browser compatibility" section of each MDN page. Edge follows the Chromium version for all Chromium features. The iOS column applies to every browser on iOS and iPadOS, since they all use WebKit.

Deciding what to build on

The matrix answers "where does it work?". The decision you actually face is "what happens to users where it does not?". Four questions settle most cases:

flowchart TD
    A["Capability needed for a feature"] --> B{"Supported in every engine your users run?"}
    B -->|Yes| C["Build on it; detect for old versions"]
    B -->|No| D{"Is there an acceptable fallback?"}
    D -->|Yes| E["Progressive enhancement: baseline + enhanced path"]
    D -->|No| F{"Can you require a browser for this feature?"}
    F -->|"Yes: internal tool, kiosk, enterprise"| G["Require Chromium; say so clearly and early"]
    F -->|No| H{"Is it the core of the product?"}
    H -->|Yes| I["Consider native, TWA wrapper, or Isolated Web App"]
    H -->|No| J["Ship without it where unsupported"]

Some practical guidance from how teams deploy these APIs:

  • Consumer apps: build on tier 1; use tier 2 and 3 as enhancements that visibly improve Chromium and Android without degrading iOS. A share button, a "save back to file" option and an app badge are good examples.
  • Hardware configurators and internal tools: a hard Chromium requirement is often acceptable (factory floors, education labs, device setup pages). Detect early and show a clear "open this page in Chrome or Edge on a computer" message instead of a broken UI.
  • Store distribution: a Trusted Web Activity inherits Chrome's capabilities on Android. It does not add capabilities that Chrome lacks.
  • Beyond the PWA security model: raw TCP/UDP sockets and similar APIs are offered only to Isolated Web Apps, which are packaged and signed and are not installed from ordinary websites.

PWA vs Native vs Hybrid and When to Build a PWA expand this decision beyond capabilities.

How to track capability status

Capability support moves with every Chromium milestone (every four weeks through Chrome 152, and every two weeks since Chrome 153 shipped on 8 September 2026, according to the Chromium release schedule) and a few times a year in Safari. Build a habit, and ideally a script, instead of relying on memory or old blog posts.

Source What it tells you Use it for
chromestatus.com Every Blink feature: stage, target milestone per platform, origin trial, links to the spec, explainer and blink-dev intent threads, and recorded Gecko, WebKit and web-developer signals Planning around Chromium, finding origin trials
Mozilla standards positions Mozilla's verdict (positive, neutral, negative, defer) with rationale Predicting whether Firefox will ship
WebKit standards positions WebKit's verdict (support, neutral, oppose) with labeled concerns; issues labeled "blocked" have no verdict yet Predicting Safari and all iOS browsers
MDN browser-compat-data Version-level support including flags, partial implementations and notes Exact support tables, automation
caniuse BCD plus its own data, usage-weighted Quick checks, audience share
webstatus.dev and web-features Baseline status (newly or widely available) across the core browser set Deciding when an API is safe as a foundation
Release notes Chrome "New in Chrome", Firefox release notes, WebKit blog and Safari Technology Preview notes Catching changes as they land

On chromestatus, the "Consensus & Standardization" block of a feature entry records the other engines' signals with links to the positions issues, and each Chrome stage links the blink-dev threads: Intent to Prototype, Intent to Experiment (the origin trial), and Intent to Ship. Reading the Intent to Ship thread is the fastest way to learn the edge cases and the concerns reviewers raised.

Automating the check

MDN publishes its compatibility data as a single JSON file. A small script can print the support for the exact capabilities your app uses, so you can run it in CI or before a planning meeting instead of re-reading tables.

scripts/capability-report.mjs
// Node 18+ (global fetch). Usage: node scripts/capability-report.mjs
// Prints browser support for the capabilities this app depends on, from MDN's browser-compat-data.

const BCD_URL = "https://unpkg.com/@mdn/browser-compat-data/data.json";

// Map human-readable names to BCD paths. Add every capability your app feature-detects.
const FEATURES = {
  "Web Share": "api.Navigator.share",
  "Share target (manifest)": "manifests.webapp.share_target",
  "File System Access": "api.Window.showOpenFilePicker",
  "File handlers (manifest)": "manifests.webapp.file_handlers",
  "Badging": "api.Navigator.setAppBadge",
  "Screen Wake Lock": "api.WakeLock",
  "Web Serial": "api.Serial",
  "Window Controls Overlay": "api.WindowControlsOverlay",
};

const BROWSERS = ["chrome", "chrome_android", "edge", "firefox", "firefox_android", "safari", "safari_ios"];

function lookup(data, path) {
  return path.split(".").reduce((node, key) => node?.[key], data)?.__compat;
}

function describe(statement) {
  // BCD support statements can be arrays (newest first) or single objects.
  const s = Array.isArray(statement) ? statement[0] : statement;
  if (!s || !s.version_added) return "no";
  if (s.version_removed) return `removed in ${s.version_removed}`;
  let text = String(s.version_added);
  if (s.flags) text += " (flag)";
  if (s.partial_implementation) text += " (partial)";
  return text;
}

async function main() {
  const response = await fetch(BCD_URL);
  if (!response.ok) throw new Error(`Failed to download BCD: HTTP ${response.status}`);
  const data = await response.json();
  console.log(`MDN browser-compat-data ${data.__meta.version} (${data.__meta.timestamp})`);

  const rows = {};
  for (const [name, path] of Object.entries(FEATURES)) {
    const compat = lookup(data, path);
    if (!compat) {
      rows[name] = { error: `unknown BCD path ${path}` };
      continue;
    }
    rows[name] = Object.fromEntries(BROWSERS.map((b) => [b, describe(compat.support[b])]));
    if (compat.status?.experimental) rows[name].experimental = "yes";
  }
  console.table(rows);
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

Common pitfalls

  • Feature detecting too shallowly. "setAppBadge" in navigator is true on Chrome for Android and Linux, where nothing appears. "share" in navigator is true in Firefox for Android, which cannot share files. Test the input with canShare() and know which platforms are no-ops.
  • Spending the activation budget. Any network request or slow computation between the click and share(), showSaveFilePicker() or requestDevice() risks the 5-second limit. Prepare data ahead of the gesture.
  • Calling two activation-consuming APIs from one click. The first consumes activation for the whole frame tree; the second fails with NotAllowedError.
  • Treating TypeError from permissions.query() as "denied". It means the browser does not know the name. Fall back to calling the API from a gesture.
  • Forgetting cross-origin iframes. Embedded widgets need allow="..." delegation for web-share, clipboard-write, screen-wake-lock, serial and the rest; the default allowlist is self.
  • Testing declarative features in a tab. Share targets, file handlers, protocol handlers, shortcuts and Window Controls Overlay do nothing until the app is installed, and manifest changes need the update flow in App Identity & Updates before they reach the OS.
  • Assuming a Chromium feature is on every Chromium platform. Check desktop versus Android versus ChromeOS versus Linux separately, and remember Android WebView.
  • Depending on an origin trial. Trials end, can be globally disabled at 0.5% of page loads, and service workers silently lack the feature if the token is only in a <meta> tag.
  • Prompting on load. Permission prompts without context get denied, Chromium switches sites with low notification acceptance to a quieter prompt, and grants disappear when Chromium removes permissions from unused sites. Ask at the moment of use.

Debugging capabilities

  • Is it a secure context? Log isSecureContext. Staging servers on plain HTTP or unusual hostnames are the usual cause of "the API is undefined".
  • Is it allowed in this frame? Chrome DevTools > Application > Frames lists the frame's permissions-policy allowed and disallowed features. In Chromium, document.featurePolicy.allowedFeatures() returns the list from the console.
  • What is the permission state? Query it (see the helper above), and reset it from the site-information icon in the address bar or chrome://settings/content.
  • Did activation expire? Log navigator.userActivation.isActive right before the call.
  • Is the origin trial active? Application > Frames > Origin trials shows token status per frame; check the service worker's response headers for the Origin-Trial header.
  • Is the OS integration registered? chrome://web-app-internals shows installed apps with their parsed manifest, including file handlers, protocol handlers, share target and launch_handler. On Android, chrome://webapks shows WebAPK state and lets you request an update.
  • Device APIs have their own internals pages (about://bluetooth-internals, about://usb-internals, about://device-log), described in Hardware & Device APIs. General DevTools workflows are in Browser DevTools.

Pages in this section

  • Web Share API


    navigator.share() and canShare(): text, URLs and files, the user-activation rule, per-platform support, and fallbacks for Linux, Firefox desktop and WebView.

    Web Share API

  • Web Share Target


    Receive shares from other apps with share_target: GET vs POST, multipart files in the service worker, the 303 redirect, and WebAPK behavior on Android.

    Web Share Target

  • File System Access


    File and directory pickers, handles, writable streams, persistent permissions, drag and drop, FileSystemObserver, and fallbacks for Firefox and Safari.

    File System Access

  • File Handling


    Register your installed PWA as an "Open with" target with file_handlers and receive files through window.launchQueue.

    File Handling

  • Protocol Handlers & Launch Handling


    registerProtocolHandler(), manifest protocol_handlers, launch_handler, link capturing on Chromium desktop, and safe parsing of launch URLs.

    Protocol Handlers & Launch Handling

  • Window Controls Overlay


    Draw into the title bar of a desktop PWA: display_override, the titlebar-area environment variables, the JavaScript API and draggable regions.

    Window Controls Overlay

  • Hardware & Device APIs


    Web Bluetooth, WebUSB, WebHID, Web Serial, Web NFC, sensors, Gamepad, Web MIDI and more, with the chooser security model and complete drivers.

    Hardware & Device APIs

  • Media & System APIs


    Screen Wake Lock, Media Session, Async Clipboard, Contact Picker, Idle Detection, Window Management, Local Font Access, EyeDropper and related system integrations.

    Media & System APIs

  • Payments


    Payment Request, Payment Handler, Apple Pay and Google Pay on the web, Secure Payment Confirmation, and digital goods in store-distributed PWAs.

    Payments

  • Authentication & Passkeys


    WebAuthn and passkeys, conditional UI, FedCM, the Digital Credentials API and credential behavior inside installed app windows.

    Authentication & Passkeys

Further reading

On this site

External references


  1. Safari 16.4 supports Screen Wake Lock on macOS and in iOS and iPadOS Safari tabs. From 16.4 to 18.3 it did not work in standalone Home Screen web apps (WebKit bug 254545); it works there from iOS/iPadOS 18.4. ↩

  2. Web Push in Safari on macOS arrived in Safari 16.1 on macOS 13 Ventura; MDN's data lists 16. ↩