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
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; asetTimeout()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, andDataErrorto four different messages, and never replace the document for any of them. - Keep
navigator.clipboard.writeText(), adownloadlink, 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
- Start from an explicit user action
- Validate the share data before the handoff
- Treat cancellation as an ordinary end state
- Keep copy and download alternatives first-class
- Make the fallback survive interruption
- Review contexts, support, and outcomes
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.
| Member | Safe source | Remove before the call |
|---|---|---|
title | document.title or a visible heading | record identifiers, account labels |
text | a selection from the rendered document | form values, private notes, tokens |
url | canonicalUrl() without secret parameters | the fragment, signed download parameters |
files | an export the person just generated | drafts, 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.
| Rejection | Meaning | Page response |
|---|---|---|
AbortError | sheet dismissed, or no target available | keep the document, return focus, stay quiet |
NotAllowedError | activation expired, or a blocked share policy | explain that sharing is blocked here |
TypeError | invalid members, empty data, unusable address | reveal the alternative, fix the builder |
DataError | a file could not be read or shared | name 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.
| Alternative | Primitive | Available when |
|---|---|---|
| copy link | navigator.clipboard.writeText(url) | secure context plus a fresh gesture |
| download | <a download="statement.txt" href="blob:..."> | the export is ready |
| plain link | an ordinary <a href> in the document | always |
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.
| Case | Setup | Expected result |
|---|---|---|
| happy path | secure page, real click | the sheet opens and the promise settles |
| cancellation | sheet dismissed | AbortError, document intact |
| missing API | navigator.share deleted | alternative visible from first paint |
| blocked policy | <iframe> without allow="web-share" | NotAllowedError, alternative remains |
| invalid data | canonicalUrl() returns a relative path | TypeError, 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.
This article is part of the guide: Web Platform Surfaces: A Consistency Review Map
Related Articles
Web Share API: Permission Boundaries and Safe Fallbacks
PlatformWeb Share Target PWA Ingress and Privacy
PlatformBadging API for PWA notification boundaries
PlatformEyeDropper API: User Activation and Color Privacy
PlatformIdle Detection API: User Control, Permissions, and Privacy Boundaries
PlatformPayment Request API: Consent and Safe Fallbacks
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.