Web Share API vs Clipboard API vs Custom Share Modal

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

Every share button on the web is one of three implementations wearing the same icon: a call to the native OS share sheet, a silent copy-to-clipboard, or a hand-built list of network links. Choosing wrong is expensive — a native-only button is dead in desktop Firefox and most in-app WebViews, a clipboard-only button gives the user no target app and no idea what to do next, and a modal-only button throws away the one-tap native flow that mobile users now expect. This guide compares the three approaches head-to-head and gives you a decision framework plus a hybrid implementation that feature-detects and layers all three.

The decision tree below routes you from your requirements to the approach that satisfies them, and then to the fallbacks that cover everything native sharing cannot.

Sharing Approach Decision Tree A decision tree that starts from the developer's requirements — file sharing, per-network analytics, legacy browser support — and routes to the Web Share API, the Clipboard API, or a custom share modal, with a hybrid layering path at the bottom. Need to share? Need to share files? Need per-target analytics? Support desktop Firefox / WebViews? Web Share API navigator.share({ files }) Custom modal tagged intent URLs Clipboard + modal universal fallback Native first then clipboard Hybrid: feature-detect native → Clipboard fallback → custom modal baseline one controller, three layers, works everywhere Yes No Yes No Yes No

Problem Framing

The three approaches are not interchangeable — each optimises for a different constraint, and picking one in isolation trades away something you probably need.

  • Web Share API (navigator.share) opens the operating system’s native share sheet. The user gets every installed app as a target, you can attach files, and the whole interaction is one tap. The price: it exists only in a secure context, it must fire from a user gesture, desktop Firefox does not implement it at all, and the Promise resolves without ever telling you which app the user picked — so per-target analytics are impossible.
  • Clipboard API (navigator.clipboard.writeText) copies the share URL to the clipboard. It works in nearly every modern browser, needs no target app, and is silent and fast. The cost is that copying is not sharing: the user still has to switch apps, paste, and add their own context. It also needs document focus and can fail quietly when the tab is backgrounded.
  • Custom share modal is your own rendered list of network links — per-network intent URLs like https://wa.me/?text=..., mailto:, sms:, X, LinkedIn, and so on. You get full control over branding, ordering, and analytics, and it works in every browser including desktop Firefox. The cost is maintenance (every network changes its URL scheme eventually), no native file sharing, and more code to own.

The reason this choice matters is that a share button is a conversion surface. If it silently fails or dead-ends, the content does not travel. The correct production answer is almost never one approach — it is a layered hybrid that uses the best available mechanism per environment, which the decision framework and hybrid implementation below make concrete.

Prerequisites Checklist

Before wiring up any of the three approaches, confirm:

Browser Support Snapshot

Support as of mid-2026. The pattern to read out of this table: native share is strong on mobile and patchy on desktop, the clipboard is nearly universal, and a custom modal is the only row with no gaps.

Browser / Platform navigator.share navigator.clipboard.writeText Custom modal (intent URLs)
Chrome for Android 75+ ✅ (files 86+)
Safari iOS 12.2+ ✅ (14.1+)
Chrome Desktop 89+ ✅ (105+ full)
Edge Desktop 93+
Firefox Desktop (all)
Firefox for Android 79+
Samsung Internet 11+
Safari macOS 12+ ✅ (no files UI parity)
In-app WebViews (IG, TikTok) ❌ (most strip it) ✅ (usually)

The full per-version detail, including flags and engine gaps, lives in the browser support matrix for the Web Share API. The takeaway for a decision: you can never rely on navigator.share alone, and the custom modal is the only mechanism guaranteed to render in every one of these rows.

Comparison Matrix

Scoring each approach against the criteria that usually decide the choice. “Native share” is navigator.share, “Clipboard” is navigator.clipboard.writeText, “Custom modal” is your own intent-URL list.

Criterion Web Share API Clipboard API Custom Share Modal
Browser support Good on mobile, gaps on desktop Firefox and WebViews Near-universal in modern browsers Universal — plain links work everywhere
File sharing ✅ via canShare({ files }) ❌ text only ❌ text/URL only
Native OS targets ✅ every installed app ❌ none — user pastes manually ❌ only networks you hand-code
Per-target analytics ❌ resolves blind, no chosen target ⚠️ “copied” event only ✅ full attribution per network
Branding / control ❌ OS-controlled sheet ❌ invisible action ✅ complete control of UI
Works offline ✅ sheet opens; delivery is the target app’s job ✅ copy is local ⚠️ link opens but target site needs network
Implementation effort Low — one call + guards Low — one call + guards High — per-network URLs, UI, maintenance
Accessibility ✅ OS handles the sheet a11y ⚠️ needs a visible “copied” confirmation ⚠️ you own focus trap, roles, keyboard nav
Interruption cost One tap, no context switch Silent, but user must paste elsewhere Modal open + choose + new tab

Read the matrix as a set of trade-offs, not a winner. Native share dominates effort, targets, and files but cannot give you analytics or branding. The custom modal wins control and analytics but costs the most to build and cannot carry files. The clipboard is the cheapest safety net but is the weakest as a primary mechanism because copying is not, by itself, sharing.

Decision Framework

Do not choose one approach — choose an ordered chain, and let feature detection pick the highest available rung at runtime.

  1. Start from requirements. If you must share files, native share is mandatory as the primary path; nothing else on the web carries binary payloads. If you must attribute shares to specific networks, a custom modal with tagged intent URLs is mandatory because navigator.share never reveals the target.
  2. Default to native-first with progressive fallback. For the common “share this link/article” case, prefer navigator.share where available. It is the lowest-effort, highest-conversion path on mobile.
  3. Fall back to the Clipboard API. When native share is absent (desktop Firefox, WebViews) or the user explicitly wants a link, copy the URL with navigator.clipboard.writeText and show a visible confirmation. This is the fastest universal action. The dedicated patterns are covered in copy-to-clipboard fallback patterns.
  4. Provide the custom modal as the baseline. Underneath both, offer a modal of per-network intent links plus mailto:/sms: bridges. It renders in 100% of browsers and is where analytics and branding live. The URI-construction details for the email and text bridges are in SMS and email fallback architectures.
  5. Never dead-end. Every branch must terminate in a working action. If the clipboard write fails and native share is absent, the modal must already be on screen.

In practice this collapses to a single rule: feature-detect native share, use it when present, and always keep a clipboard-plus-modal fallback wired underneath so no environment is left without a share path.

Step-by-Step Implementation

Each approach below leads with the mandatory secure-context and feature-detection guard, then the minimal correct call. The final step composes all three into a hybrid controller.

Step 1 — Web Share API with a feature-detection guard

Confirm the secure context and the callable API before touching navigator.share. Validate the payload with navigator.canShare when it exists, which is essential for file payloads.

// native-share.js
export function canUseNativeShare(payload) {
  if (window.isSecureContext !== true) return false;
  if (typeof navigator.share !== 'function') return false;
  if (typeof navigator.canShare === 'function') {
    return navigator.canShare(payload);
  }
  // Older browsers with share but no canShare: reject files, allow text/url.
  if (payload?.files?.length) return false;
  return Boolean(payload?.title || payload?.text || payload?.url);
}

export async function shareNatively(payload) {
  if (!canUseNativeShare(payload)) {
    return { status: 'unsupported' };
  }
  try {
    await navigator.share(payload);
    return { status: 'shared' };
  } catch (error) {
    if (error.name === 'AbortError') return { status: 'dismissed' };
    return { status: 'error', errorName: error.name };
  }
}

AbortError means the user opened then closed the sheet — a clean cancel, not a failure, so it must not trigger a fallback. Any other error name should fall through to the next layer.

Step 2 — Clipboard API copy fallback

The clipboard write is guarded the same way: secure context, then the callable method. Keep the call inside the user gesture, since a backgrounded or unfocused document rejects the write.

// clipboard-share.js
export function canUseClipboard() {
  return (
    window.isSecureContext === true &&
    typeof navigator.clipboard?.writeText === 'function'
  );
}

export async function copyShareUrl(url) {
  if (!canUseClipboard()) {
    return { status: 'unsupported' };
  }
  try {
    await navigator.clipboard.writeText(String(url));
    return { status: 'copied' };
  } catch (error) {
    // NotAllowedError: document not focused, or permission blocked.
    return { status: 'error', errorName: error.name };
  }
}

For rich-text or HTML clipboard payloads rather than a plain share URL, the ClipboardItem approach is covered in mastering the Clipboard API for rich text. For a share button, writeText with the canonical URL is the right primitive.

Step 3 — Custom share modal with tagged intent URLs

The modal owns branding and analytics. Each network gets a builder that encodes the payload into that network’s URL scheme; opening the link in a new tab keeps your page intact. Because these are plain links, this step has no feature-detection gate — it is the universal baseline — but the encoding must be correct.

// share-modal.js
const NETWORKS = {
  whatsapp: ({ text, url }) =>
    `https://wa.me/?text=${encodeURIComponent(`${text} ${url}`.trim())}`,
  x: ({ text, url }) =>
    `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`,
  linkedin: ({ url }) =>
    `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
  email: ({ title, text, url }) =>
    `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(`${text}\n\n${url}`)}`,
};

export function buildIntentUrl(network, payload) {
  const builder = NETWORKS[network];
  if (!builder) throw new TypeError(`Unknown share network: ${network}`);
  return builder(payload);
}

export function openIntent(network, payload, onShare) {
  const href = buildIntentUrl(network, payload);
  onShare?.({ event: 'share_intent', network }); // full per-network analytics
  window.open(href, '_blank', 'noopener');
}

Because you control this list, you can log share_intent with the exact network — the attribution that navigator.share can never provide.

Step 4 — The hybrid controller layering all three

This is the recommended production shape. One entry point feature-detects native share, falls back to the clipboard, and always keeps the modal available as the baseline. It never dead-ends.

// hybrid-share.js
import { shareNatively } from './native-share.js';
import { copyShareUrl } from './clipboard-share.js';

export async function share(payload, ui) {
  // Layer 1: native OS sheet, the preferred path where supported.
  const native = await shareNatively(payload);
  if (native.status === 'shared') {
    ui.track?.({ event: 'share_native' });
    return 'shared';
  }
  if (native.status === 'dismissed') {
    return 'dismissed'; // user cancelled — respect it, do not fall through
  }

  // Layer 2: clipboard copy for a fast universal action.
  const copied = await copyShareUrl(payload.url);
  if (copied.status === 'copied') {
    ui.track?.({ event: 'share_clipboard' });
    ui.showCopiedToast?.();
  }

  // Layer 3: always surface the branded modal as the guaranteed baseline.
  ui.openModal?.(payload);
  return 'fallback';
}

The canShare pre-flight and secure-context checks all live inside the layer modules, so the controller reads as pure orchestration. This mirrors the ordered fallback tree described for navigator.canShare graceful fallbacks.

Payload Validation and Error Boundary

Validate once, at the entry point, so every layer receives a clean payload. A malformed URL breaks intent links and the clipboard just as surely as it breaks navigator.share.

// validate-payload.js
export function buildValidPayload({ title = '', text = '', url = location.href }) {
  const cleanUrl = String(url).trim();
  try {
    const parsed = new URL(cleanUrl, location.href);
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      throw new TypeError(`Unsupported URL scheme: ${parsed.protocol}`);
    }
    return { title: String(title).trim(), text: String(text).trim(), url: parsed.href };
  } catch {
    throw new TypeError(`Invalid share URL: ${cleanUrl}`);
  }
}

The errors each layer can raise, and the correct response:

Source Error / status Cause Correct response
navigator.share AbortError User dismissed the sheet Stop — do not fall through to clipboard/modal
navigator.share NotAllowedError No gesture on the stack, or policy block Fall through to clipboard, then modal
navigator.share TypeError Empty or invalid payload / rejected file Fix payload; route to modal for text-only share
navigator.share DataError File MIME type rejected by the OS Route to modal; offer a download instead
navigator.clipboard NotAllowedError Document unfocused or permission denied Skip toast; modal is already the baseline
Custom modal TypeError Unknown network key passed to builder Programmer error — fail loudly in dev

The one rule that catches most bugs: AbortError from native share is a successful user decision, not a failure. Treating it as a failure and popping the modal is the most common regression in hybrid implementations.

Platform Gotchas

iOS Safari — native share is the strong path. On iPhone, navigator.share is present, supports files, and is what users expect. The clipboard write works but iOS shows its own “Copied” affordance only in some contexts, so render your own visible toast. iPad user-agent strings often lack Mobi, so device heuristics that assume “no Mobi means desktop” will misroute iPads.

Android Chrome — everything works, prefer native. All three layers function. Watch pop-up handling in the modal: window.open for intent URLs is reliable inside a gesture but can be blocked from a deferred callback. Keep openIntent synchronous with the click.

Desktop — native share is inconsistent. Chrome and Edge on desktop support navigator.share (Chrome 105+ for full support), but the sheet lists far fewer targets than mobile, and desktop Firefox has no support at all. On desktop, the clipboard-plus-modal combination is usually the better primary experience even where native share technically exists.

In-app WebViews — assume native share is stripped. Instagram, TikTok, and Facebook WebViews commonly remove navigator.share and can intercept scheme navigation. The custom modal and clipboard are your only reliable layers there, which is another reason the modal must always be present.

Standalone PWAs. Installed PWAs in standalone display mode can restrict cross-app navigation, affecting mailto:/sms: links opened from the modal. Prefer window.open(href, '_blank') over assigning location.href for those intents inside an installed app.

Testing and Verification

DevTools checklist:

  1. In the console, confirm window.isSecureContext === true, then log typeof navigator.share and typeof navigator.clipboard?.writeText to see which layers the current environment exposes.
  2. Set the user-agent override to a Firefox Desktop string. Confirm canUseNativeShare() returns false and that a share attempt lands on the clipboard copy and opens the modal.
  3. Trigger a native share (default UA), open then dismiss the sheet, and confirm the result is dismissed and that the modal does not appear.
  4. Break the payload URL to a javascript: scheme and confirm buildValidPayload throws before any layer runs.

Physical device checklist:

Analytics verification: confirm that share_native, share_clipboard, and share_intent (with a network property) arrive in telemetry. Because native share is analytics-blind, a healthy split shows native events on mobile and per-network intent events concentrated on desktop and WebViews. If you see zero share_intent events on desktop, the modal is likely never rendering — check the fallback wiring.

FAQ

Should I use the Web Share API or a custom share modal?

Use the Web Share API as the primary path where it is available, because it offers native targets, file support, and a single tap. Layer a custom modal underneath as the universal fallback for desktop Firefox, in-app WebViews, and any environment where navigator.share is undefined. The two are complementary — the hybrid controller above uses native share first and keeps the modal as the guaranteed baseline.

Can I get analytics on which app the user shared to with navigator.share?

No. navigator.share resolves with an empty Promise and never reveals the chosen target. You only know the share succeeded, not whether it went to Messages, WhatsApp, or email. If per-network attribution is a hard requirement, a custom modal with tagged intent URLs is the only approach that provides it, which is why the decision framework routes analytics-critical features to the modal.

Does the Clipboard API need a permission prompt?

navigator.clipboard.writeText() usually needs only a user gesture and document focus, not an explicit permission dialog, on Chrome, Edge, and Safari. Reading from the clipboard requires the clipboard-read permission, but writing a share URL rarely prompts. Always call it inside a click handler and wrap it in try/catch to handle focus-related failures gracefully.

Why does my share button do nothing in Firefox on desktop?

Firefox on desktop does not implement navigator.share, so the property is undefined and an unchecked call throws a TypeError. This is exactly why you feature-detect first and fall through to the Clipboard API or a custom modal. Never assume the API exists just because the browser is modern — the browser support matrix documents every gap.

Can the custom modal share files like the native sheet?

No. Per-network intent URLs and mailto:/sms: links carry text and URLs only, not binary file payloads. File sharing is exclusive to the Web Share API via navigator.canShare({ files }). If files are central to your feature, native share is the only web platform path, and you must provide a download fallback in every environment where it is unavailable.