Browser Support Matrix for Web Share API
This guide is part of Web Share API & Security Contexts.
navigator.share bridges the web and the OS-level share sheet, but its surface area is fragmented: mobile browsers shipped it years before desktop, file sharing arrived later still, and Firefox desktop has not implemented it at all. Without an accurate map of what each browser actually supports — and a deterministic fallback when it does not — you will silently break sharing for a significant portion of your audience.
The sections below walk through the full implementation lifecycle: prerequisites, the support matrix itself, step-by-step feature detection, payload validation, error categorisation, platform-specific gotchas, and how to verify your implementation on real devices.
Prerequisites
Before invoking the API, confirm all three conditions are true in your environment:
- Secure context — the page is served over HTTPS or from
localhost/127.0.0.1. The browser will not exposenavigator.shareat all on an insecure origin. See Understanding Secure Context Requirements for origin-level configuration details. - User gesture — the call must originate from a direct
clickortouchstartevent. Calls insidesetTimeout,Promise.then, or any async hop that severs the gesture token will throwNotAllowedError. - Supported browser — feature detection is mandatory; the table below maps minimum versions and known gaps.
No browser permission dialog is shown to the user — there is no permission to “grant”. The OS share sheet appears directly after invocation if all three conditions are met.
Browser Support Snapshot
Key takeaway: Mobile is broadly covered — Android Chrome, iOS Safari, Samsung Internet, Firefox for Android (v151+), and Edge all support both text/URL and file sharing. Desktop Firefox remains the significant gap and requires a fallback path for every user on that browser.
Step-by-Step Implementation
Step 1 — Synchronous feature gate
Never call navigator.share without first verifying it exists. A missing check causes TypeError on Firefox desktop and any browser that predates the API. This gate must run synchronously, before any async work.
/**
* Returns true when the current environment supports Web Share
* for the given payload, false otherwise.
*
* @param {ShareData} data - The intended share payload
* @returns {boolean}
*/
export function isShareSupported(data) {
if (typeof navigator.share !== 'function') return false;
// navigator.canShare validates payload schema AND OS-level MIME acceptance.
// Learn more: /web-share-api-security-contexts/understanding-secure-context-requirements/
if (typeof navigator.canShare === 'function') {
return navigator.canShare(data);
}
// Older browsers that have share but not canShare: assume text/url payloads work.
return Boolean(data?.text || data?.url);
}
Step 2 — Secure context and environment validation
The secure context requirement means navigator.share is undefined on any HTTP origin (except localhost). Validate before rendering the share button so you never show a control that cannot work.
/**
* Returns a configuration object describing share availability
* for the current origin and environment.
*
* @returns {{ enabled: boolean, mode: 'prod' | 'dev' | 'fallback' }}
*/
export function getShareEnvironmentConfig() {
if (!window.isSecureContext) {
console.warn('[ShareAPI] Insecure origin — native sharing disabled.');
return { enabled: false, mode: 'fallback' };
}
const isLocalhost =
location.hostname === 'localhost' ||
location.hostname === '127.0.0.1';
return { enabled: true, mode: isLocalhost ? 'dev' : 'prod' };
}
Note: window.isSecureContext is already true for localhost and 127.0.0.1 — the isLocalhost flag is purely informational for telemetry, not a conditional gate.
Step 3 — Payload construction and canShare pre-check
A valid ShareData object must contain at least one of title, text, url, or files. Desktop browsers restrict files more aggressively than mobile; always run navigator.canShare(data) before calling navigator.share(data).
Implementing navigator.canShare for graceful fallbacks covers the full validation pattern, including MIME-type filtering across platforms.
interface ValidatedSharePayload {
title?: string;
text?: string;
url?: string;
files?: File[];
}
/**
* Validates a share payload shape and URL format before invocation.
*
* @throws {TypeError} if the payload is empty or contains an invalid URL
*/
export function validateSharePayload(input: unknown): ValidatedSharePayload {
if (!input || typeof input !== 'object') {
throw new TypeError('Share payload must be a non-null object.');
}
const { title, text, url, files } = input as ValidatedSharePayload;
const hasRequiredField = Boolean(text || url || files?.length);
if (!hasRequiredField) {
throw new TypeError(
'Payload must contain at least one of: text, url, or files.'
);
}
if (url) {
try {
new URL(url);
} catch {
throw new TypeError(`Invalid URL in share payload: "${url}"`);
}
}
return { title, text, url, files };
}
Step 4 — Invoke navigator.share and map rejections
interface ShareResult {
status: 'success' | 'cancelled' | 'error';
code?: string;
}
/**
* Invokes navigator.share after capability and payload checks,
* and returns a structured result for telemetry.
*
* Must be called directly from a user gesture handler.
*
* @param {ShareData} data - Pre-validated share payload
* @returns {Promise<ShareResult>}
*/
export async function invokeShare(data: ShareData): Promise<ShareResult> {
if (!isShareSupported(data)) {
return { status: 'error', code: 'UNSUPPORTED' };
}
try {
await navigator.share(data);
return { status: 'success' };
} catch (error) {
if (error instanceof DOMException) {
switch (error.name) {
case 'AbortError':
// User dismissed the share sheet — not a failure.
return { status: 'cancelled' };
case 'NotAllowedError':
// Gesture token expired or policy block.
return { status: 'error', code: 'NOT_ALLOWED' };
case 'TypeError':
// Payload rejected by the OS (MIME type, empty object, etc.).
return { status: 'error', code: 'INVALID_PAYLOAD' };
default:
return { status: 'error', code: error.name };
}
}
return { status: 'error', code: 'UNKNOWN' };
}
}
Payload Validation and Error Boundary
navigator.share() returns a Promise that resolves when the OS accepts the payload for dispatch (not when the recipient receives it). Rejections fall into three distinct categories — treat them differently in your telemetry and UI:
| Rejection | Cause | User-visible symptom | Recovery |
|---|---|---|---|
AbortError |
User cancelled the share sheet | Share sheet closes, nothing sent | No action — expected interaction |
TypeError |
Empty payload, invalid URL, or MIME type rejected by OS | Share button appears to do nothing | Validate payload with canShare() before showing the button |
NotAllowedError |
No user gesture, or platform/policy block | Share fails silently | Ensure call is in a direct event handler; offer fallback UI |
Never log AbortError as an application error. Doing so inflates error rates and obscures real failures.
Platform Gotchas
iOS Safari
iOS Safari enforces MIME types strictly via navigator.canShare. Files not in the allowed list (image/png, image/jpeg, video/mp4, audio/mpeg, PDFs, and a handful of others) will cause canShare to return false. Safari also requires that the share invocation happen in the same task as the triggering user event — any await between the gesture and navigator.share() will throw NotAllowedError on iOS 14 and earlier. From iOS 15 onward the gesture token survives a single await, but the safest pattern is to call navigator.share() as the first await in your handler.
Android Chrome
Chrome on Android is the most permissive implementation. It validates MIME types but accepts a broad set. One common trap: if you pass a files array where even one file fails canShare, the entire call is rejected. Filter files individually before assembling the payload.
Chrome and Edge on Desktop
Desktop Chrome (89+) and Edge (Chromium, 79+) route shares through the OS share dialog, which requires at least one registered share target application on the machine. On a freshly installed Windows or macOS system with no share targets, the share sheet may appear empty or the call may silently fail. Always provide a fallback — the QR code fallback is a good option for cross-device scenarios.
Firefox for Android
Firefox Android added navigator.share in version 151. The set of available share targets varies by device manufacturer and installed apps. Treat it the same as Android Chrome in your feature-detection logic.
Firefox Desktop
Firefox desktop does not implement navigator.share. Your feature-detection gate will return false for the entire user base on this browser. Route all users through your fallback chain immediately — do not show a disabled share button or a bare error message.
macOS Safari (desktop)
macOS Safari 12.1 introduced navigator.share for text and URLs; file sharing arrived in 15.1. The system share sheet on macOS Ventura and later is robust, but older macOS versions (Monterey and earlier) may show a more limited target list. Test on physical hardware or a VM rather than relying on device simulators alone.
Graceful Degradation and Fallback Chain
When isShareSupported returns false, degrade through this ordered chain. The SMS and email fallback architectures guide covers the URI-scheme fallbacks in depth.
/**
* Executes the fallback share chain when navigator.share is unavailable.
* Tries Clipboard API, then mailto:, then custom modal UI.
*
* @param {{ title?: string, text?: string, url?: string }} data
* @returns {Promise<{ status: string }>}
*/
export async function executeShareFallback(data) {
// 1. Clipboard API — fastest path for URL/text payloads.
if (navigator.clipboard?.writeText) {
const copyText = data.url || data.text || '';
try {
await navigator.clipboard.writeText(copyText);
return { status: 'copied' };
} catch {
// Permission denied or non-secure context — continue.
}
}
// 2. mailto: URI — works cross-platform on desktop without permissions.
if (data.url || data.text) {
const subject = encodeURIComponent(data.title || '');
const body = encodeURIComponent(data.url || data.text || '');
window.location.href = `mailto:?subject=${subject}&body=${body}`;
return { status: 'mailto_triggered' };
}
// 3. Custom share modal UI — last resort, always possible.
renderCustomShareModal(data);
return { status: 'custom_ui_rendered' };
}
Decision Flow: Native Share or Fallback?
Testing and Verification
DevTools checklist
- Open DevTools → Application → Manifest. Confirm the page is flagged as a secure context.
- In the Console, run
window.isSecureContext— must returntrue. - Run
navigator.share— must return a function reference, notundefined. - Run
navigator.canShare({ url: 'https://example.com' })— must returntrue. - Trigger the share button. Confirm the OS sheet opens and
AbortErroris thrown (not logged as an error) when you dismiss it.
Physical device checklist
Testing in Chrome DevTools’ device emulation is not sufficient — the share sheet is an OS-level UI that emulation cannot replicate.
- Android Chrome: Install the PWA or open in mobile Chrome. Tap Share. Verify the native share sheet appears with app icons. Test with a
filespayload using an image to confirm MIME validation. - iOS Safari: Open in Safari on a physical iPhone. Tap Share. The iOS share sheet must appear — simulators on macOS do invoke the sheet but with a limited app list. Verify file sharing with a JPEG on iOS 15+.
- macOS Safari: Confirm the system share sheet opens on a Mac running Ventura or later. Older macOS versions may show fewer targets.
- Firefox for Android: Install Firefox 151+ and confirm the share sheet appears. Note that the available targets depend on installed apps.
- Firefox Desktop: Confirm
isShareSupportedreturnsfalseand your fallback UI renders correctly.
Automated integration test sketch
// vitest / jest example — run in a jsdom environment with navigator mocked.
import { isShareSupported, getShareEnvironmentConfig } from './share-utils.js';
describe('isShareSupported', () => {
it('returns false when navigator.share is absent', () => {
Object.defineProperty(navigator, 'share', { value: undefined, configurable: true });
expect(isShareSupported({ url: 'https://example.com' })).toBe(false);
});
it('delegates to canShare when present', () => {
Object.defineProperty(navigator, 'share', { value: () => Promise.resolve(), configurable: true });
Object.defineProperty(navigator, 'canShare', { value: () => true, configurable: true });
expect(isShareSupported({ url: 'https://example.com' })).toBe(true);
});
});
describe('getShareEnvironmentConfig', () => {
it('returns fallback mode on non-secure context', () => {
Object.defineProperty(window, 'isSecureContext', { value: false, configurable: true });
expect(getShareEnvironmentConfig().enabled).toBe(false);
});
});
FAQ
Does the Web Share API require explicit user permission prompts?
No. The API enforces an implicit user-gesture requirement rather than an explicit permission dialog. Browsers validate that the call originates from a trusted click or touchstart event and that the origin is secure (HTTPS or localhost). No permission entry appears in browser settings for navigator.share.
Why does navigator.share fail on localhost during development?
It typically does not — modern browsers treat localhost as a secure context, so window.isSecureContext is true. If it fails locally, confirm the share call sits directly inside a click handler and is not wrapped in a setTimeout or a long async chain that severs the gesture token. Also check that you are not serving from a non-localhost hostname (e.g. a local IP address) without TLS — see configuring HTTPS for local development.
Is file sharing supported across all browsers?
No. Files via the files array require Android Chrome 86+, iOS Safari 15+, macOS Safari 15.1+, or Chrome 89+ on desktop. Firefox for Android added support in version 151; Firefox desktop has not implemented the API. Always validate with navigator.canShare({ files: [...] }) before showing a file-share UI element.
How do I handle Web Share API failures in PWAs?
Distinguish AbortError (user cancelled — not an error) from TypeError (malformed payload) and NotAllowedError (missing gesture or policy block). Chain a deterministic fallback: Clipboard API first, then mailto:/sms: URI schemes, then a custom modal UI. The offline share queue implementation covers queuing shares that fail due to network absence.
Related
- Web Share API & Security Contexts — parent section
- Understanding Secure Context Requirements — origin configuration and
isSecureContextsemantics - Implementing navigator.canShare for Graceful Fallbacks — MIME validation patterns
- Configuring HTTPS for Local Development — serve over HTTPS locally to unlock the full API
- SMS and Email Fallback Architectures — URI-scheme fallback chain details
- QR Code Generation for Cross-Device Sharing — alternative sharing path for desktop users