Back to Knowledge Hub
Platform

navigator.canShare() and the Web Share API: Sharing Boundaries

How navigator.canShare() and navigator.share() decide whether a share is accepted, where permissions and file limits differ, and how to test both.

BotBrowser Team

Documentation

Want the structured docs for Platform?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

Sharing on the web is a handoff to a surface the page does not control. navigator.share() asks the browser to pass a title, a URL, text, or a set of files to whatever target the person picks, and the page learns only that the returned promise settled. BotBrowser can load a mobile profile in a controlled context to review how an application lays out its share control and its alternatives across viewports and locales; it cannot render the operating system share sheet, cannot pick a target, and cannot grant the transient activation a share call requires. When the destination is an installed PWA, Web Share Target PWA ingress and privacy explains how to validate the receiving route without treating delivery as permission to retain content.

TL;DR

  • Call navigator.share() from a trusted click or key press; a setTimeout() delay or a page-load call lets transient activation expire.
  • Build the shared object from what the person can already see, and strip the fragment, session parameters, tokens, and record identifiers first.
  • Map AbortError, NotAllowedError, TypeError, and DataError to four different messages, and never replace the document for any of them.
  • Keep navigator.clipboard.writeText(), a download link, and the plain URL visible before the first attempt, not after a rejection.
  • BotBrowser reviews the responsive flow around the control in a selected profile; the sheet, the target list, and the activation decision stay with the browser and the operating system.

Contents

A user chooses content, reviews a share handoff, and can use a fallback

The W3C Web Share specification defines the sharing model and the security requirements it inherited from the platform. MDN's Web Share API guide, navigator.share(), and navigator.canShare() describe the browser-facing methods and the compatibility notes. All three documents describe a conditional capability. None of them promises one target, one sheet layout, or one result across platforms.

Start from an explicit user action

navigator.share() runs only inside transient user activation. The call has to happen while the browser still honors the activation granted to a trusted event, so a setTimeout() with even a short delay, a scroll listener, or an automatic call after DOMContentLoaded ends in a NotAllowedError rejection. Attach the call to the click and keydown handlers of one control and let that control own the whole attempt.

Feature detection stays explicit. typeof navigator.share === 'function' is the check worth running. Reading navigator.canShare without calling it proves nothing, because the method is present on browsers that accept only the text and URL members. Render the control when the method exists and window.isSecureContext is true, and otherwise render a disabled control with a short note that the page must be served over https.

const shareButton = document.querySelector('[data-share]');

if (window.isSecureContext && typeof navigator.share === 'function') {
  shareButton.hidden = false;
  shareButton.addEventListener('click', event => {
    event.preventDefault();
    startShare(shareButton);
  });
}

async function startShare(button) {
  const data = { title: document.title, url: canonicalUrl() };
  if (!navigator.canShare(data)) return revealAlternative('copy');
  button.setAttribute('aria-busy', 'true');
  try {
    await navigator.share(data);
    announce('Share sheet closed');
  } catch (error) {
    announce(shareMessage(error));
  } finally {
    button.removeAttribute('aria-busy');
    button.focus();
  }
}

The handler never awaits anything before the call, because a promise settled first can consume the activation the browser granted to the click. canonicalUrl() should return an absolute address built from new URL(location.href). A relative path, an empty string, or a javascript: value fails validation and turns a routine click into a TypeError.

Name the control after the action, for example "Share this article" rather than "Share". An icon-only trigger needs an aria-label and a tooltip, and its hit area should stay at least 24 by 24 CSS pixels so it still meets the WCAG 2.2 target size minimum when the page is zoomed to 200 percent. A native <button> element already forwards Enter and Space to the same handler, which keeps the keyboard path identical to the pointer path without extra code.

Do not pre-open a sheet, prefetch a target list, or infer a recipient from the browser locale. The person picks the destination inside the browser or the operating system surface, and the page has no visibility into that choice.

Validate the share data before the handoff

The share object accepts title, text, url, and a files array. Each member is optional, at least one must carry data, and a call whose members are empty or malformed rejects with TypeError. Build the object from state the person can see instead of from internal session data.

MemberSafe sourceRemove before the call
titledocument.title or a visible headingrecord identifiers, account labels
texta selection from the rendered documentform values, private notes, tokens
urlcanonicalUrl() without secret parametersthe fragment, signed download parameters
filesan export the person just generateddrafts, temporary paths, another person's data

navigator.canShare() answers one narrow question: will this browser accept this shape of data. It does not answer whether an installed target will take it. A false result belongs in the alternative path before the sheet opens, which is cheaper than a rejection the person has to interpret. Files need a second check, because canShare(data) can be true while canShare({ files: [file] }) is false on a browser that shares text and links only.

For a file, name the object and declare its type: new File([blob], 'statement-2026-10.txt', { type: 'text/plain' }). A File without a readable type can pass the check and still fail at the receiving application. Keep the export inside the size limit your own download path already enforces, and call URL.revokeObjectURL() once the promise settles, so a large report does not stay referenced after a cancellation.

Sanitize the address itself, not only the path. location.href carries the fragment and every query parameter, including a short-lived token added by a redirect. Rebuild the shared address from url.origin, url.pathname, and an allow-listed url.search, so the fragment and the session parameters never reach navigator.share(). A share that leaks an access_token is a credential leak with a friendly button attached.

Authorization does not move. The recipient opens the address in a new browsing context, where the server must repeat the same session check, the same origin rule, and the same 403 path as for a pasted link. Credential Management API: Design A Safer Browser Sign-In Flow covers how a browser-mediated sign-in keeps the server in charge, and Passkeys and WebAuthn: The Browser Sign-In Journey explains where the authenticator proves possession. Neither changes the rule that a shared address is untrusted input on arrival.

Treat cancellation as an ordinary end state

The promise resolves when the sheet closes after a handoff and rejects when the person cancels. Chromium reports cancellation as AbortError, and the same name covers the case where the browser found no target to offer. Both outcomes are normal, so an alert() at that moment interrupts the task for no reason at all.

RejectionMeaningPage response
AbortErrorsheet dismissed, or no target availablekeep the document, return focus, stay quiet
NotAllowedErroractivation expired, or a blocked share policyexplain that sharing is blocked here
TypeErrorinvalid members, empty data, unusable addressreveal the alternative, fix the builder
DataErrora file could not be read or sharedname the file, offer the download

Never retry automatically. The activation that made the first call valid is spent, so a retry loop produces a chain of NotAllowedError rejections and, in some browsers, no visible reaction at all. Wait for the next click, which is also the moment the person re-confirms the intent.

Restore the interface without moving anyone. Focus returns to the element that opened the sheet, the scroll offset stays where it was, and unsaved editor content survives untouched. Announce the outcome through role="status" with aria-live="polite", so a screen reader hears "Nothing was shared" once instead of interrupting the sentence being read. Pages that also speak their messages can follow Speech Synthesis Voices and Accessible Browser Playback, which covers voice selection and interrupt behavior.

A pending state should be short-lived and reversible. Setting button.disabled = true removes the control from the tab order and can swallow the click that started the call, so prefer aria-busy on a button that stays focusable. A local target usually settles the promise in under a second, but a remote destination can hold the sheet open, and the page has to remain usable while the promise is pending.

Record the outcome, never the content. Counters named share-completed, share-cancelled, and share-unavailable describe the flow without carrying a title, an address, or a file name into a log. Keep that record short-lived, and do not treat share availability as a signal about the account, the device, or the network.

Keep copy and download alternatives first-class

The alternative path carries the same intent through a surface the page controls, and it belongs on screen beside the share control rather than behind a rejection. Three representations cover most products.

AlternativePrimitiveAvailable when
copy linknavigator.clipboard.writeText(url)secure context plus a fresh gesture
download<a download="statement.txt" href="blob:...">the export is ready
plain linkan ordinary <a href> in the documentalways

Clipboard writes fail more often than people expect. navigator.clipboard is undefined outside a secure context, writeText() rejects with NotAllowedError when the gesture has expired, and some browsers show a permission prompt that can be declined. A copy control therefore needs two visible states, copied and not copied, plus a selectable read-only field as the last resort. A visible input with the address selected is honest; a silent failure that claims success is not. The Clipboard API and user intent guide extends this copy fallback into a separate read-permission and data-minimization decision.

A download needs a file name and a format the receiving application can open. Create the blob, set URL.createObjectURL(blob) on the anchor, and revoke it when the download finishes or the page unloads. If generation is slow, show progress and keep the original content readable, because a disabled button with no explanation is worse than a stated wait of a few seconds.

Keep the three actions in one group with distinct labels: copy, download, and share. Visual grouping is fine, while permissions and outcomes are not interchangeable. A copy keeps the content on the current device, a share leaves the page, and an invite would create an account relationship that needs its own confirmation and authorization.

Two related boundaries matter here. Browser permission boundaries explains why capability checks belong next to the code that consumes the result, and privacy-aware browser workflows covers how much state a page should retain after a task ends. An alternative that reuses the same sanitized address as the share call inherits both decisions, which is why the address builder belongs in one function.

Make the fallback survive interruption

Network state is not evidence of a completed handoff. The browser can pass data to a local application without a single page request, and a remote destination may need its own connection afterwards. The page therefore knows only that it asked, which makes "share sheet opened" or "share cancelled" honest labels and "delivered" a claim the code cannot support.

Interruption changes what the person expects. A tab that becomes hidden during a pending promise can resume with the sheet already gone, so re-read the control state on visibilitychange instead of assuming the finally block ran while the document was visible. Keep the pending flag in a <form>-safe place, and let a late rejection update only a message that is still on screen.

Offline and intermittent connections do not change the interaction model. A local article can still offer a copyable address while the network is down, and an export can wait until the file finishes generating. Show whether the representation is ready, keep the current content readable, and avoid a disabled control with no reason attached.

Sharing often sits beside print, save, and invite. Give each action a distinct label and state material differences. Print keeps the content in the current tab, save stores a local copy, share hands data to another application, and invite may create an account relationship that needs confirmation. Grouping them visually is reasonable; merging their permissions is not.

Language changes what fits. A long translated title wraps the button label, right-to-left text reorders an icon placed with a fixed margin, and a non-Latin script changes the measured width of the same string. Test with the longest locale you ship, keep the document lang attribute accurate, and translate status messages together with labels so a cancellation never appears in a second language on the same screen.

Review contexts, support, and outcomes

Run the same small outcome matrix in every browser family you support, and keep the rows as the product contract rather than as a description of one vendor's sheet.

CaseSetupExpected result
happy pathsecure page, real clickthe sheet opens and the promise settles
cancellationsheet dismissedAbortError, document intact
missing APInavigator.share deletedalternative visible from first paint
blocked policy<iframe> without allow="web-share"NotAllowedError, alternative remains
invalid datacanonicalUrl() returns a relative pathTypeError, alternative remains

An embedded document needs allow="web-share" on the <iframe> and, where the host sends the header, Permissions-Policy: web-share=(self). Without either, the call rejects with NotAllowedError even after a real click, and the component has to keep its own copy path. The same holds for a page opened inside a webview that does not forward the directive.

Automated tests can stub the method in three shapes: a function that resolves, one that rejects with new DOMException('cancelled', 'AbortError'), and no method at all. Assert the visible state rather than the call count, so the assertions cover an alternative that is reachable, a draft that survives, and a status message that appears once. Constructing a DOMException keeps the name property the handler branches on, which a plain Error does not.

Measure the path you own. Mark a performance.mark('share-complete') entry when the alternative finishes, and read it with PerformanceObserver during a support session instead of guessing from user reports. PerformanceObserver for Browser Application Health shows how to keep that measurement local and short-lived.

Sources

Practical conclusion

Treat sharing as one optional branch of a task that must finish without it. Gate navigator.share() on a trusted event, publish a sanitized absolute address, and map AbortError, NotAllowedError, TypeError, and DataError to four distinct and quiet responses.

Keep navigator.clipboard.writeText(), a download link, and the plain address visible from first paint, so a missing target, a blocked policy, or a cancelled sheet costs the reader nothing. BotBrowser supports mobile-profile browser workflows for reviewing an application's responsive sharing flow, but it does not simulate the operating system share sheet or its targets and does not guarantee user activation.

#Web Share API#Sharing#User Activation#Accessibility#Privacy

This article is part of the guide: Web Platform Surfaces: A Consistency Review Map

Run the same check in BotBrowser

Load a profile, keep the signals you just read about consistent inside the browser, and confirm the result on your own machine before a rollout.