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 detection —
navigator.sharemust 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 validation —
navigator.canShare(payload)must returntruebeforenavigator.share()is invoked; passing an unsupported combination oftitle,text,url, andfilesthrowsTypeErroror silently fails on some platforms. - User gesture — every
navigator.share()call must be synchronously inside aclick,tap, orkeydownhandler; the browser rejects invocations that are not traceable to a direct gesture withNotAllowedError.
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
canUseNativeShare(data) === true→ callnavigator.share(data)navigator.clipboard?.writeTextavailable → copy to clipboard, show confirmation- Platform is mobile (touch primary) → construct
sms:URI with pre-filled body - Desktop detected → offer
mailto:link and copyable text input - 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 aFilewith a MIME type that the receiving app cannot open causes silent rejection —canSharereturnstruebutshareresolves without opening the dialog. Test with the exact MIME type list your UI surfaces. - Chrome on Linux: Even where
navigator.shareexists, 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.
Related
- Understanding Secure Context Requirements — deep dive into
window.isSecureContext, TLS validation, and iframe isolation - Configuring HTTPS for Local Development — mkcert, Vite HTTPS, and testing on physical devices
- Browser Support Matrix for Web Share API — minimum versions, flags, and known engine gaps by platform
- Permission Flows & Progressive Enhancement — contextual prompts, denial handling, and the full fallback decision tree
- Native Device Integration Patterns — File System Access API, Clipboard API, Web NFC, and async payload patterns