../

Web Share

navigator.share() and canShare() for handing text, links and files to the OS share sheet, the fallbacks you need where it's missing, and the manifest share_target that makes an installed app a share destination. Support data is from MDN as of September 2026.

Support

navigator.share() is not Baseline (MDN: limited availability): it is solid on mobile and uneven on desktop.

Browsershare()filescanShare()Share target (manifest)
Chrome / Edge Android61+76+75+76+, installed apps
Chrome / Edge desktop128+ (Windows and ChromeOS since 89)89+128+89+, installed apps
Safari macOS / iOS12.1 / 12.214+14+no
Firefox Android79+no96+no
Firefox desktopbehind a flagnobehind a flagno

Always feature-detect; never branch on the user agent. A desktop Firefox user needs the fallback.

Requirements

RequirementDetail
secure contextHTTPS or localhost; navigator.share is undefined otherwise
transient activationcall from a click or key handler; a short await first is fine, a slow one isn't
Permissions Policyweb-share; cross-origin iframes need allow="web-share"
one at a timea second call while the sheet is open rejects with InvalidStateError
no permission promptthere is nothing to query in the Permissions API

API

MemberReturnsNotes
navigator.share(data)Promise<void>resolves when the user picks a target (Android may resolve earlier)
navigator.canShare(data?)booleantrue if share(data) would be accepted; no activation needed
ShareData fieldTypeNotes
titlestringoften ignored by the target
textstringmessage body; many apps put everything here
urlstringabsolute or relative (resolved against the page); "" = current page
filesFile[]images, video, audio, PDF, text; see Sharing files

At least one field is required. share({}) rejects with TypeError.

declare const btn: HTMLButtonElement;
 
btn.addEventListener("click", async () => {
  try {
    await navigator.share({
      title: "Fetch API cheatsheet",
      text: "Typed fetch patterns",
      url: "/typescript/web-apis/fetch-api/",
    });
  } catch (err) {
    if (err instanceof DOMException &&
        err.name === "AbortError") return; // canceled
    throw err;
  }
});

Sharing files

declare const blob: Blob; // e.g. from canvas.toBlob
 
const file = new File([blob], "chart.png", {
  type: "image/png",
});
const data: ShareData = { files: [file], title: "Q3 chart" };
 
if (navigator.canShare?.(data)) {
  await navigator.share(data);
}
RuleDetail
check with canShare({ files })the only reliable test; share existing doesn't imply file support
give files a name and an extensionChromium checks the extension against an allowlist
allowed types (Chromium)common image, audio and video formats, PDF, and text files such as .txt, .csv, .html
blocked typesexecutables, archives, unknown types: NotAllowedError or TypeError
sizeno fixed limit, but large files may fail on mobile; keep to a few MB
mixing files with url/textallowed, but some targets drop one or the other; test on devices

Errors

ErrorCauseHandle
AbortErroruser closed the sheet, or no targets availableignore silently
NotAllowedErrorno user activation, web-share policy blocks it, file type blockedfall back
TypeErrorempty data, bad URL, unsupported filesa bug; check canShare first
InvalidStateErrora share is already open, or the document isn't activedisable the button while sharing
DataErrorthe target app failed to start or receive the datafall back or retry

Fallbacks

FallbackWhen
copy link to the clipboarddefault desktop fallback; see Clipboard
mailto: linkmailto:?subject=...&body=... with encodeURIComponent
download the filewhen canShare({ files }) is false
service-specific share URLsonly if you need those networks; they change over time
function mailtoHref(subject: string, body: string) {
  const q = new URLSearchParams({ subject, body });
  // mailto wants %20, not +
  return `mailto:?${q.toString().replaceAll("+", "%20")}`;
}

Web Share Target

An installed web app (Chromium only) can appear in the OS share sheet by declaring share_target in its manifest. Text and links can use GET; files need POST with multipart/form-data.

manifest.webmanifest
{
  "share_target": {
    "action": "/share-target",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [
        { "name": "media", "accept": ["image/*", "video/*"] }
      ]
    }
  }
}
FieldMeaning
actionURL inside the app's scope that receives the share
methodGET (query string) or POST
enctypeapplication/x-www-form-urlencoded or multipart/form-data (files)
paramsmaps title, text, url to your parameter names
files{ name, accept } list; accept takes MIME types or extensions

Android apps often put the shared URL in text and leave url empty, so parse both. A POST target must be handled by the service worker (or the server), which then redirects with 303.

Typing in TS

SituationFix
navigator.share typed as always presentdetect with typeof navigator.share === "function"
navigator.canSharesame; navigator.canShare?.(data) is fine at runtime
caught errorsunknown; narrow with err instanceof DOMException and name
ShareData.filesFile[], not FileList; spread an input's files: [...input.files]
share_target handler in the SWevent.request.formData() gives FormData; getAll("media") is FormDataEntryValue[]

Recipes

Share button with fallback

Native sheet where it exists, copy-to-clipboard everywhere else.

type Shared = "shared" | "copied" | "canceled";
 
export async function shareLink(
  data: { title: string; url: string },
): Promise<Shared> {
  if (typeof navigator.share === "function" &&
      navigator.canShare?.(data) !== false) {
    try {
      await navigator.share(data);
      return "shared";
    } catch (err) {
      const name = err instanceof DOMException && err.name;
      if (name === "AbortError") return "canceled";
      // NotAllowedError etc.: fall through to copy
    }
  }
  const url = new URL(data.url, location.href).href;
  await navigator.clipboard.writeText(url);
  return "copied"; // show a "Link copied" toast
}

Share a generated image

Render a canvas, share it as a PNG file, or download it when file sharing isn't supported.

function toBlob(canvas: HTMLCanvasElement) {
  return new Promise<Blob>((resolve, reject) =>
    canvas.toBlob(
      (b) => (b ? resolve(b) : reject(new Error("empty"))),
      "image/png",
    ),
  );
}
 
export async function shareCanvas(c: HTMLCanvasElement) {
  const blob = await toBlob(c);
  const file = new File([blob], "score.png", {
    type: "image/png",
  });
  if (navigator.canShare?.({ files: [file] })) {
    return navigator.share({ files: [file] });
  }
  const a = document.createElement("a");
  a.href = URL.createObjectURL(blob);
  a.download = file.name;
  a.click();
  setTimeout(() => URL.revokeObjectURL(a.href), 1_000);
}

toBlob is fast enough to keep the click's activation; for slow rendering, prepare the file before the user clicks.

Show the share button only when useful

Hide the native button where it would always fall back, and label it for the platform.

declare const shareBtn: HTMLButtonElement;
declare const copyBtn: HTMLButtonElement;
 
const canNativeShare =
  window.isSecureContext &&
  typeof navigator.share === "function";
 
shareBtn.hidden = !canNativeShare;
copyBtn.hidden = canNativeShare;

Receive shared files in the service worker

Handle a POST share target: stash the files, then redirect to a page that shows them.

sw-share-target.ts
/// <reference lib="webworker" />
declare const self: ServiceWorkerGlobalScope;
 
self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);
  if (url.pathname !== "/share-target") return;
  if (event.request.method !== "POST") return;
  event.respondWith(
    (async () => {
      const form = await event.request.formData();
      const files = form.getAll("media")
        .filter((v): v is File => v instanceof File);
      const cache = await caches.open("shared");
      await Promise.all(files.map((f, i) =>
        cache.put(`/shared/${i}`, new Response(f)),
      ));
      return Response.redirect("/inbox?shared=1", 303);
    })(),
  );
});

References