Web Share API & Security Contexts

The Web Share API gives web applications direct access to the operating system’s native share sheet, routing content to any installed app the user chooses. This reference covers every architectural constraint that must be satisfied before navigator.share() will fire — from TLS certificates and origin isolation through payload validation, cross-browser quirks, and the full fallback chain that keeps your UI functional when native sharing is blocked.

Core Architecture Mandates

Before any share trigger is wired up, four invariants must hold:

  • Secure origin — the page must be served over HTTPS or from localhost / 127.0.0.1; mixed-content pages are treated as insecure even if the top frame is HTTPS.
  • Feature detectionnavigator.share must be confirmed as a callable function; its presence is never guaranteed, and sandbox attributes or permission policies can strip it even on valid origins.
  • Payload validationnavigator.canShare(payload) must return true before navigator.share() is invoked; passing an unsupported combination of title, text, url, and files throws TypeError or silently fails on some platforms.
  • User gesture — every navigator.share() call must be synchronously inside a click, tap, or keydown handler; the browser rejects invocations that are not traceable to a direct gesture with NotAllowedError.

Web Share API Security Context Decision Flow A flowchart showing the four validation gates — secure context, feature detection, payload canShare, and user gesture — before navigator.share() is called, with fallback paths at each gate. Secure context? window.isSecureContext API available? typeof navigator.share Payload valid? navigator.canShare(data) User gesture present? (click / tap / keydown) Invocation must be synchronous inside the event handler navigator.share(data) Native OS share sheet opens Fallback Clipboard / modal NO → fallback YES YES

API Surface Reference

The complete interface exposed by the Web Share API specification:

Method / Property Signature Returns Throws
navigator.share() share(data?: ShareData): Promise<void> Promise<void> resolved on success AbortError (user dismissed), NotAllowedError (no gesture or policy block), TypeError (invalid payload)
navigator.canShare() canShare(data?: ShareData): boolean true if platform accepts payload — (never throws; absent in older browsers)
window.isSecureContext readonly boolean true on HTTPS / localhost

ShareData interface:

Property Type Notes
title string (optional) Shown as share title; not all targets display it
text string (optional) Body copy for share targets that accept text
url string (optional) Must be a valid URL; relative paths are resolved against location.href
files File[] (optional) Requires canShare({ files }) check; varies by OS

At least one of title, text, url, or files must be present. Passing an empty object throws TypeError.


Establishing Secure Origin Checks

Modern browsers enforce strict origin isolation to prevent cross-site data leakage. The Web Share API operates exclusively within secure contexts — a valid TLS certificate or the explicit localhost / 127.0.0.1 exemption granted by the browser. Mixed content policies block share triggers on insecure sub-resources even when the top-level frame is HTTPS; staging environments without proper certificates throw NotAllowedError or silently disable the API.

Validate window.isSecureContext before rendering any share UI. To test secure APIs across local network devices such as physical phones and tablets, you need a trusted local certificate — see Configuring HTTPS for Local Development for the exact setup workflow.

/**
 * Validates the secure context before exposing share capabilities.
 * Call this once during page initialisation; cache the result.
 * @returns {boolean} Whether the environment supports secure sharing.
 */
export function isSecureShareContext() {
  if (typeof window === 'undefined') return false;

  if (!window.isSecureContext) {
    console.warn(
      'Web Share API requires a secure context (HTTPS or localhost).' +
      ' Current origin: ' + location.origin
    );
    return false;
  }

  return true;
}

Iframe contexts introduce an additional constraint. A same-origin iframe inherits the parent’s secure context, but a cross-origin iframe does not expose navigator.share unless the embedding page adds allow="web-share" to the <iframe> element via the Permissions Policy. Detect this with the same typeof navigator.share === 'function' check — the attribute controls whether the API surface is present, not just whether it’s callable.


Implementing Feature Detection

After confirming the secure context, verify that navigator.share is a callable function. Browser support for the Web Share API has improved substantially, but Firefox on desktop still lacks support (as of mid-2026), and Chromium-based browsers on Linux only added support in version 89. The browser support matrix covers minimum versions and platform-specific gaps in full.

Detection logic must also account for sandbox restrictions that strip API access on valid origins. Cache the detection result — repeated navigator property lookups are cheap, but the semantics of the check should not change between calls for the same page lifetime.

/**
 * Checks API availability, sandbox restrictions, and payload compatibility.
 * Secure-context guard runs first — this is non-negotiable.
 * @param {ShareData} payload
 * @returns {boolean} Whether navigator.share can handle this exact payload.
 */
export function canUseNativeShare(payload) {
  if (!isSecureShareContext()) return false;
  if (typeof navigator.share !== 'function') return false;

  // canShare validates payload schema and OS-level compatibility.
  // It is absent in some older Chromium builds — check before calling.
  if (typeof navigator.canShare === 'function') {
    return navigator.canShare(payload);
  }

  // Conservative fallback: assume basic text/url payloads are supported
  // when canShare is unavailable; reject file payloads.
  if (payload?.files?.length) return false;
  return Boolean(payload?.text || payload?.url);
}

For complex payloads that include files, always use navigator.canShare({ files: [...] }) as a pre-flight — iOS Safari rejects certain MIME types silently, and Android Chrome imposes a per-file size cap that differs from the total payload cap.


Architecting Invocation Patterns

Share triggers must align with explicit user intent. The specification mandates that invocations originate from a direct user gesture. Payload validation using navigator.canShare() prevents malformed requests from reaching the native dialog. Implement a transient loading state on the trigger element to prevent duplicate activations while the share promise resolves — tapping a share button twice before the first dialog appears causes a NotAllowedError on some platforms because the second call lands outside the gesture window.

/**
 * Full share invocation with gesture-synchronous entry, payload guard,
 * duplicate-tap prevention, and typed error boundaries.
 * @param {ShareData} data - Pre-validated payload
 * @param {HTMLElement} trigger - The button that received the user gesture
 * @returns {Promise<'shared' | 'dismissed' | 'fallback'>}
 */
export async function invokeShare(data, trigger) {
  if (!canUseNativeShare(data)) return 'fallback';

  // Prevent duplicate activation while the dialog is pending.
  if (trigger.dataset.sharing === 'true') return 'dismissed';
  trigger.dataset.sharing = 'true';
  trigger.setAttribute('aria-busy', 'true');

  try {
    await navigator.share(data);
    return 'shared';
  } catch (error) {
    // AbortError means the user dismissed the dialog — not a failure state.
    if (error.name === 'AbortError') return 'dismissed';

    // TypeError indicates an invalid payload structure or missing fields.
    if (error instanceof TypeError) {
      console.error('Invalid share payload:', error.message, data);
      return 'fallback';
    }

    // NotAllowedError: missing gesture, browser policy block, or rate limit.
    console.error('Share blocked:', error.name, error.message);
    return 'fallback';
  } finally {
    delete trigger.dataset.sharing;
    trigger.removeAttribute('aria-busy');
  }
}

For content that needs async payload formatting — such as generating a canonical URL or resizing a file before sharing — resolve the data synchronously inside the gesture handler, then pass the fully-constructed ShareData to invokeShare. Deferring construction into an await chain inside the handler consumes the gesture token on some browsers and causes NotAllowedError on the subsequent navigator.share() call.


Designing Fallback Strategies

When native sharing is unavailable or blocked, the UX must degrade gracefully through an ordered decision tree. The primary fallback for URL and text payloads is navigator.clipboard.writeText(). Custom share modals using mailto: and sms: URI schemes provide deterministic cross-platform bridges — the SMS and email fallback reference covers URI construction patterns and encoding requirements.

Progressive enhancement means baseline functionality is guaranteed regardless of browser support. Never block the main thread while resolving fallback operations; expose fallback UI synchronously rather than waiting for an async check.

Ordered Fallback Decision Tree

  1. canUseNativeShare(data) === true → call navigator.share(data)
  2. navigator.clipboard?.writeText available → copy to clipboard, show confirmation
  3. Platform is mobile (touch primary) → construct sms: URI with pre-filled body
  4. Desktop detected → offer mailto: link and copyable text input
  5. None of the above → open a custom modal with copy + QR code

For offline scenarios where the share queue must survive a connectivity gap, see the offline share queue implementation guide, which covers IndexedDB persistence and Service Worker background sync to drain queued payloads when the connection restores.

/**
 * Progressive fallback chain for environments where native share is blocked.
 * @param {ShareData} data
 * @param {{ onCopied?: () => void, onModal?: () => void }} callbacks
 * @returns {Promise<void>}
 */
export async function fallbackShare(data, callbacks = {}) {
  const text = data.url ?? data.text ?? '';

  // 1. Try clipboard first — available on most modern browsers.
  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      callbacks.onCopied?.();
      return;
    } catch {
      // Clipboard blocked (e.g. no focus) — continue to next fallback.
    }
  }

  // 2. SMS URI on mobile (heuristic: touchscreen primary + small viewport).
  const isMobile = navigator.maxTouchPoints > 1 && window.innerWidth < 768;
  if (isMobile && text) {
    const encoded = encodeURIComponent(`${data.title ? data.title + ': ' : ''}${text}`);
    // 'sms:' separator is '?' on iOS, '&' on Android — use '?' as the safer default.
    location.href = `sms:?body=${encoded}`;
    return;
  }

  // 3. Custom modal / QR code — caller decides rendering.
  callbacks.onModal?.();
}

Cross-Browser Behaviour

Platform-specific quirks table for the Web Share API as of mid-2026:

Browser / Platform navigator.share navigator.canShare File sharing Notes
Chrome 89+ / Android Yes Yes Yes Full support; ~50 MB per-file cap
Chrome 89+ / Desktop (CrOS, Win, Mac) Yes (105+) Yes Yes Desktop added in 105; Linux support fragile
Safari 14+ / iOS Yes Yes (15+) Yes ~10 MB per file; some MIME types rejected silently
Safari 14+ / macOS Yes Yes Partial Files require user to have accepted a share permission implicitly
Samsung Internet 11.4+ Yes Partial Yes canShare returns true for unsupported combos on older versions
Firefox / Desktop No No No No support as of Firefox 127; use fallback unconditionally
Firefox / Android Yes (experimental) Partial No Enabled by flag; not production-stable
Edge 81+ / Desktop Yes Yes Yes Inherits Chromium behaviour; requires same gesture constraints

Key platform gotchas:

  • iOS Safari and files: Passing a File with a MIME type that the receiving app cannot open causes silent rejection — canShare returns true but share resolves without opening the dialog. Test with the exact MIME type list your UI surfaces.
  • Chrome on Linux: Even where navigator.share exists, the native dialog does not always present system apps — it falls back to a browser-internal share UI.
  • Permissions Policy in iframes: The allow="web-share" attribute is required on any <iframe> element, even same-origin ones in some Chromium versions. Add it defensively.
  • Page visibility: On iOS, moving the page to the background mid-share causes the promise to reject with AbortError, not stay pending.

Progressive Enhancement Strategy

The full decision tree from API availability through fallback routing:

Step 1 — Gate on secure context at page load

// Run once at module initialisation; do not repeat per share invocation.
const SECURE = isSecureShareContext();
const NATIVE_SHARE = SECURE && typeof navigator.share === 'function';

Step 2 — Render UI conditionally based on capability

/**
 * Renders the appropriate share trigger based on environment capability.
 * @param {ShareData} payload
 * @param {HTMLElement} container
 */
export function renderShareUI(payload, container) {
  if (!isSecureShareContext()) {
    // HTTP or insecure context — show mailto link only.
    container.innerHTML = `<a href="mailto:?subject=${encodeURIComponent(payload.title ?? '')}&body=${encodeURIComponent(payload.url ?? '')}">Share via email</a>`;
    return;
  }

  if (!NATIVE_SHARE || !canUseNativeShare(payload)) {
    // HTTPS but no native share — show clipboard button.
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = 'Copy link';
    btn.addEventListener('click', () => fallbackShare(payload, {
      onCopied: () => { btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = 'Copy link'; }, 2000); }
    }));
    container.appendChild(btn);
    return;
  }

  // Full native share available.
  const btn = document.createElement('button');
  btn.type = 'button';
  btn.textContent = 'Share';
  btn.addEventListener('click', async (e) => {
    const result = await invokeShare(payload, e.currentTarget);
    if (result === 'fallback') await fallbackShare(payload);
  });
  container.appendChild(btn);
}

Step 3 — Handle result states

Result Meaning Action
'shared' User completed a share Optional analytics event
'dismissed' User opened then closed the dialog No action; do not show fallback
'fallback' Native share unavailable or errored Present clipboard / modal

Error Handling Reference

DOMException.name Cause Recovery
NotAllowedError No valid user gesture; Permissions Policy blocked the API; rate-limiting in browser Ensure invocation is synchronous inside a gesture handler; check iframe allow attribute; present fallback
AbortError User dismissed the native share sheet Not an error — treat as a clean cancel; no fallback needed
TypeError Payload empty, missing required fields, or contains an unsupported files MIME type Pre-validate with canShare(); check files array MIME types; ensure at least one of title, text, url is present
DataError (Rare) Platform-level data validation failure Log payload for debugging; route to fallback

For a walkthrough of the most common NotAllowedError scenario — navigator.share is undefined even on HTTPS — see how to fix navigator.share is undefined in Chrome.

To design permission prompts that minimise user resistance before sharing is triggered, see designing contextual permission prompts.


Frequently Asked Questions

Why does the Web Share API fail on HTTP origins?

Browsers restrict the API to secure contexts to prevent malicious sites from intercepting share payloads or spoofing native dialogs. HTTP origins lack the cryptographic guarantees required by the secure context specification. If you see navigator.share returning undefined on an HTTP staging server, the fix is always to enable TLS — see Configuring HTTPS for Local Development.

How do I handle share failures in production?

Always wrap navigator.share() in a try/catch. Distinguish AbortError (user dismissed — not a failure) from NotAllowedError (gesture or policy problem) and TypeError (payload problem). Implement the progressive fallback chain — clipboard copy or a custom share modal — to maintain UX continuity. Log NotAllowedError and TypeError to your error tracker; log AbortError only at debug level.

Can the Web Share API be triggered programmatically without user interaction?

No. The specification mandates that every navigator.share() call be synchronously traceable to a direct user gesture (click, tap, or keydown). Programmatic calls without a recent gesture, and calls made inside setTimeout or after an await that breaks the gesture chain, throw NotAllowedError. If your flow needs async work before sharing, do it before the user gesture and store the result; then call navigator.share() synchronously inside the handler.

What are the payload limits for the Web Share API?

The API accepts title, text, url, and files. File sharing limits vary by OS and browser: iOS Safari typically allows up to ~10 MB per file; Android Chrome up to ~50 MB. Some MIME types are silently rejected by iOS even when canShare() returns true. Always call navigator.canShare({ files: [...] }) before attempting to share files, and provide a download fallback for payloads that fail the check.

Does the Web Share API work inside iframes?

Only when the iframe has the allow="web-share" Permissions Policy attribute on the <iframe> element. Cross-origin iframes without that attribute do not expose navigator.share, even on a valid HTTPS origin. Same-origin iframes may work without it in some browsers but this is not guaranteed — add allow="web-share" defensively to any iframe that needs to invoke the API.

How do I implement navigator.canShare() for graceful fallbacks?

navigator.canShare(data) returns a boolean synchronously. Check typeof navigator.canShare === 'function' before calling it — the method is absent in browsers that support navigator.share but pre-date the canShare addition (notably some older Android WebViews). See the dedicated guide on implementing navigator.canShare() for graceful fallbacks for a full treatment including file MIME type validation.