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.sharereads asundefinedon an HTTP staging URL even though the browser supports the API on HTTPS.NotAllowedErrorfires in production when a share button is programmatically clicked (no real user gesture).- A
TypeErrorhalts execution becausefilesandurlwere 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.shareis stripped from cross-origin iframes and sandboxed frames withoutallow-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 aclick,tap, orkeydownhandler. 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:
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.sharewithout first checkingwindow.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 acanShareguard — this always throws aTypeErroron iOS. - Treating
AbortErroras a failure and logging it to your error tracker — inflates error rates and hides real failures. - Blocking the call inside an
asyncfunction with anawaitbeforenavigator.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.
Related
- Web Share API & Security Contexts — parent overview covering the full secure-context architecture
- Browser Support Matrix for Web Share API — minimum versions, file-sharing limits, and engine-specific quirks
- Configuring HTTPS for Local Development — set up a trusted local certificate for physical device testing
- Implementing navigator.canShare for graceful fallbacks — complete fallback decision tree with SMS, email, and clipboard paths
- How to fix navigator.share is undefined in Chrome — troubleshooting when the API appears missing despite HTTPS