Understanding Secure Context Requirements for the Web Share API

This guide is part of Web Share API & Security Contexts.

The Web Share API hands off content to the native OS share sheet — but browsers refuse to expose that surface unless the page proves it is running in a trustworthy environment. Without the correct secure-context setup, every call to navigator.share() throws a NotAllowedError before your payload ever reaches the system dialog. This guide walks through the exact validation sequence, error taxonomy, and fallback architecture you need to ship reliable share functionality across iOS, Android, and desktop.

What breaks without secure context validation

Skip the checks and you get silent failures in the best case, unhandled promise rejections and broken UI in the worst. The three most common symptoms:

  • navigator.share reads as undefined on an HTTP staging URL even though the browser supports the API on HTTPS.
  • NotAllowedError fires in production when a share button is programmatically clicked (no real user gesture).
  • A TypeError halts execution because files and url were combined in a payload on an iOS device that rejects that combination.

All three are preventable with the four-gate pattern covered in the steps below.

Prerequisites

Before writing any share code, confirm these conditions:

  • TLS certificate on every environment. Production must serve HTTPS. Staging must also — an HTTP staging URL will always fail the secure-context gate. See Configuring HTTPS for Local Development to set up a trusted local certificate for physical device testing.
  • Top-level browsing context. navigator.share is stripped from cross-origin iframes and sandboxed frames without allow-same-origin allow-popups. Verify your share trigger lives in the top frame.
  • Direct user gesture. The spec mandates that navigator.share() must be called synchronously inside a click, tap, or keydown handler. Async delays — even a single microtask — can break the gesture requirement in some browsers.
  • No enterprise content policies blocking sharing. Managed Chromium on locked-down enterprise devices may suppress the API regardless of origin.

Browser support snapshot

Browser Minimum version Desktop support Mobile support Known gaps
Chrome 61 No (share target required) Yes Desktop enabled from Chrome 89 on Windows/macOS with a registered share target
Safari 12.1 macOS 12.1+ (Safari 15.1+) Yes (iOS 12.2+) Files require iOS 15+; url+files together unsupported
Firefox No No (Android 88+ partial) navigator.share absent on most Firefox builds
Edge 79 Yes (Chromium-based) Yes Same as Chromium
Samsung Internet 8.2 Yes Older versions ignore files silently

Cross-reference platform gaps against the Browser Support Matrix for Web Share API before deciding which payload types to support at launch.

Step-by-step implementation

Step 1 — Check window.isSecureContext on module load

Evaluate the boolean once at module initialisation. If it is false, suppress native share controls immediately and render your fallback UI. Do not defer this check to the click handler — it wastes a render cycle and causes a flash of broken UI.

// shareModule.js
export function initShareModule() {
  const shareBtn = document.getElementById('share-trigger');
  const fallbackContainer = document.getElementById('share-fallback');

  if (!window.isSecureContext) {
    shareBtn?.remove();
    renderFallbackUI(fallbackContainer);
    return;
  }

  if (typeof navigator.share !== 'function') {
    shareBtn?.remove();
    renderFallbackUI(fallbackContainer);
    return;
  }

  shareBtn.addEventListener('click', (event) => {
    event.preventDefault();
    void handleShareInvocation({
      title: document.title,
      text: document.querySelector('meta[name="description"]')?.content ?? '',
      url: window.location.href
    });
  });
}

Step 2 — Validate the payload with navigator.canShare before rendering

Call navigator.canShare(data) with your intended payload during UI initialisation, not just inside the click handler. If the payload is already invalid for the current platform, swap to fallback rendering before the user sees a broken button.

export function buildSharePayload() {
  return {
    title: document.title,
    text: document.querySelector('meta[name="description"]')?.content ?? '',
    url: window.location.href
  };
}

export function isPayloadShareable(data) {
  if (typeof navigator.canShare !== 'function') {
    // canShare unavailable — assume shareable, let share() surface errors
    return true;
  }
  return navigator.canShare(data);
}

navigator.canShare() returns false silently when the payload structure is rejected (unsupported MIME type, combined url+files on iOS, empty object). It does not throw — catch the return value, not an exception.

Step 3 — Invoke navigator.share inside the gesture handler

The call must remain synchronous relative to the user gesture. Even a brief await before navigator.share() can break the gesture association in Chromium. Validate first, invoke immediately.

export async function handleShareInvocation(data) {
  if (!isPayloadShareable(data)) {
    renderFallbackUI(document.getElementById('share-fallback'));
    return;
  }

  try {
    await navigator.share(data);
    recordAnalyticsEvent('share_success', {
      platform: navigator.userAgentData?.platform ?? 'unknown'
    });
  } catch (error) {
    handleShareError(error);
  } finally {
    document.getElementById('share-trigger')?.classList.remove('is-loading');
  }
}

Step 4 — Classify and route every DOMException

Three exception names cover all Web Share API failure modes. Handle each one explicitly rather than logging a generic error — the recovery path differs for each.

function handleShareError(error) {
  switch (error.name) {
    case 'AbortError':
      // User closed the native dialog intentionally — not a failure.
      // Do not log to error tracking. Do not show a fallback.
      break;

    case 'NotAllowedError':
      // Two causes: no user gesture, or browser/OS blocked the API.
      // Route to fallback UI and log for ops visibility.
      console.error('[share] NotAllowedError — gesture or permission blocked:', error);
      renderFallbackUI(document.getElementById('share-fallback'));
      break;

    case 'TypeError':
      // Malformed payload: missing required field, unsupported MIME type,
      // or url+files combined on iOS.
      console.error('[share] TypeError — invalid payload:', error.message);
      renderFallbackUI(document.getElementById('share-fallback'));
      break;

    default:
      console.error('[share] Unhandled exception:', error);
      renderFallbackUI(document.getElementById('share-fallback'));
  }
}

For a deeper breakdown of when navigator.share returns undefined vs. throws, see How to fix navigator.share is undefined in Chrome.

Step 5 — Render deterministic fallback UI

Fallbacks must be deterministic — not dependent on network state or user consent. mailto:, sms:, and navigator.clipboard.writeText() are the three always-available alternatives. Build the fallback container into the HTML so it renders instantly without a DOM insertion round-trip.

export function renderFallbackUI(container) {
  if (!container) return;

  const encodedUrl = encodeURIComponent(window.location.href);
  const encodedTitle = encodeURIComponent(document.title);

  container.innerHTML = `
    <button
      type="button"
      class="fallback-copy-btn"
      onclick="navigator.clipboard.writeText(window.location.href)"
    >Copy link</button>
    <a
      href="mailto:?subject=${encodedTitle}&body=${encodedUrl}"
      class="fallback-email-link"
    >Share via email</a>
    <a
      href="sms:?body=${encodedTitle}%20${encodedUrl}"
      class="fallback-sms-link"
    >Share via SMS</a>
  `;
  container.hidden = false;
}

For a complete progressive-enhancement fallback strategy including implementing navigator.canShare for graceful fallbacks, see the dedicated guide.

Step 6 — Wire everything together

// Entry point — call once after DOMContentLoaded.
export function initShareModule() {
  const shareBtn = document.getElementById('share-trigger');
  const fallbackContainer = document.getElementById('share-fallback');

  const isSecure = window.isSecureContext;
  const hasShareAPI = typeof navigator.share === 'function';
  const payload = buildSharePayload();
  const payloadValid = isPayloadShareable(payload);

  if (!isSecure || !hasShareAPI || !payloadValid) {
    shareBtn?.remove();
    renderFallbackUI(fallbackContainer);
    return;
  }

  shareBtn.addEventListener('click', (event) => {
    event.preventDefault();
    shareBtn.classList.add('is-loading');
    void handleShareInvocation(payload);
  });
}

document.addEventListener('DOMContentLoaded', initShareModule);

Secure context validation flow

The diagram below maps the decision path from page load to either a successful OS share sheet handoff or a rendered fallback:

Secure context and feature detection decision flow A flowchart showing the four validation gates — isSecureContext, navigator.share exists, navigator.canShare, user gesture — before navigator.share() is invoked, with fallback UI rendered at each failure point. Page load / DOMContentLoaded window.isSecureContext true? No Render fallback Yes navigator.share is function? No Render fallback Yes canShare(payload) returns true? No Render fallback Yes Attach click listener to share button navigator.share(payload) on gesture

Payload validation and error boundary

navigator.canShare() is a synchronous predicate — use it as a guard, not a diagnostic. When it returns false, do not call share(). The table below maps every relevant failure to its cause and recovery action:

Exception Root cause Recovery
NotAllowedError No user gesture, or browser/OS policy block Render fallback; log for ops monitoring
NotAllowedError HTTP origin (isSecureContext false) Already handled at gate 1; fallback already shown
TypeError Missing required field (title, text, url, files all absent) Fix payload construction; show fallback
TypeError url+files combined (iOS Safari) Remove url from payload; validate with canShare first
TypeError Unsupported MIME type in files Check against platform MIME allowlist; show fallback
AbortError User dismissed the native dialog No action; this is expected user behaviour
DataError File object corrupt or zero-byte Validate file size and type before constructing payload

Platform gotchas

iOS Safari (12.2 – 17.x). navigator.share is available but navigator.canShare was not added until iOS 15. On iOS 12–14, canShare returns undefined — fall back to assuming the payload is valid and let share() surface a TypeError if it fails. You cannot share url and files in the same call on any iOS version; split them into separate share actions or omit url when sharing files.

Android Chrome (61+). The most permissive implementation. Supports files, url, and text in a single payload. canShare is reliable from Chrome 75+. Note that the Web Share API on Android requires the allow-popups and allow-same-origin permissions if called from an iframe.

Desktop Chrome / Edge (89+). The API is available but only surfaces a share dialog when the OS has at least one registered share target application. On a fresh Windows or macOS install with no share targets registered, share() may resolve immediately without presenting any dialog — which looks like a silent success but means the content was never shared. You cannot detect this condition; analytics on share_success events will over-count on desktop.

Firefox. navigator.share is not implemented on Firefox desktop or on most Android Firefox builds as of mid-2026. Treat Firefox as “fallback only” in your capability detection.

Sandboxed iframes. The allow-popups and allow-same-origin sandbox attributes are required. Even with these, some browsers block navigator.share inside cross-origin iframes entirely. Test this boundary explicitly in your embedding context.

Testing and verification

DevTools quick-check. Open DevTools console on your page and run:

console.log({
  isSecureContext: window.isSecureContext,
  hasShare: typeof navigator.share,
  hasCanShare: typeof navigator.canShare
});

All three should read true / "function" / "function" on a properly configured HTTPS page in Chrome or Safari.

Simulate HTTP origin. In Chrome DevTools, you cannot override isSecureContext directly, but you can serve the page over HTTP locally to verify your fallback path renders correctly. Confirm shareBtn is removed from the DOM and fallbackContainer is visible.

Physical device checklist:

  • Test on an iPhone running iOS 15+ for file sharing; iOS 12–14 for text/URL sharing only.
  • Test on an Android device in Chrome to verify the native share sheet appears.
  • Test on a desktop Chrome with at least one share target installed (e.g. Gmail, Outlook).
  • Test in Firefox to confirm fallback UI renders without console errors.
  • Test in a sandboxed iframe to confirm the iframe-specific path is handled.

Gesture requirement verification. Add a 100 ms setTimeout before navigator.share() in a test build and confirm it throws NotAllowedError in Chrome — this validates that your production code is calling share() synchronously from the gesture handler.

Common pitfalls

  • Checking navigator.share without first checking window.isSecureContext — the API may appear defined in some runtime environments even on HTTP, then fail at call time.
  • Passing { files: [...], url: '...' } to iOS Safari without a canShare guard — this always throws a TypeError on iOS.
  • Treating AbortError as a failure and logging it to your error tracker — inflates error rates and hides real failures.
  • Blocking the call inside an async function with an await before navigator.share() — breaks the synchronous gesture association in some Chromium versions.
  • Omitting fallback UI for desktop Firefox and desktop Chrome without share targets — a significant portion of your desktop audience will see nothing.

FAQ

Why does navigator.share fail on localhost without HTTPS?

Modern browsers treat localhost and 127.0.0.1 as secure contexts per the W3C Secure Contexts specification. If the API fails locally, check window.isSecureContext in DevTools — an enterprise policy or misconfigured reverse proxy may be overriding the origin classification.

Can I force the Web Share API to work over HTTP?

No. The API is strictly gated behind window.isSecureContext. HTTP origins will always fail this check and the API either returns undefined or throws NotAllowedError. The only compliant paths are HTTPS in production or localhost for local development.

How do I handle file sharing securely?

Always call navigator.canShare({ files: [...] }) before invoking share(). Ensure each file is a valid File object with an explicit MIME type. iOS Safari and Android Chrome enforce different MIME type allowlists — canShare() is the only reliable compatibility gate across platforms.

What is the recommended fallback when secure context validation fails?

Render a custom share UI using mailto:, sms:, and navigator.clipboard.writeText() as deterministic alternatives. Log fallback triggers to your analytics pipeline to measure platform gaps and prioritise HTTPS migration for staging environments.

Does navigator.canShare throw if called with an invalid payload?

No. canShare() returns false for invalid payloads without throwing. The only exception is if canShare itself is called outside a secure context — in that case, some browsers throw NotAllowedError. Always check window.isSecureContext and typeof navigator.canShare === 'function' before calling it.