The Flutter Web Share API: navigator.share and share_plus on the Web
Sharing from Flutter web: navigator.share requirements, Web Share Level 2 files, browser support, share_plus fallbacks, and progressive enhancement.
Published on • November 12, 2026
AI Assistant

Sharing from a Flutter web app looks like one line until you read the spec. navigator.share() is not a general-purpose “send to any app” API: it only exists in a secure context, it only runs from a recent user gesture, and on a large slice of the desktop web it does not exist at all. share_plus (v13.3.1 on pub.dev, from the fluttercommunity.dev publisher) hides most of that behind SharePlus.instance.share(), but its web branch has rules of its own.
What the browser demands before it will share
Three gates come first. navigator.share() requires a secure context, so on plain HTTP or an IP-address host the method is simply absent. It is gated by the web-share Permissions Policy, which throws when the permission is supported but not granted. And it requires transient activation: MDN states the call “must be triggered off a UI event like a button click and cannot be launched at arbitrary points by a script.”
// Inside onPressed: activation is live, the share sheet opens.
ElevatedButton(
onPressed: () => _shareArticle(),
child: const Text('Share'),
);
// From a timer or a stream event: rejected with NotAllowedError.
Timer.periodic(const Duration(seconds: 3), (_) => _shareArticle());
The activation window is short, so keep the path from tap to share() short as well: no network await in between, no initState, no addPostFrameCallback. Share and copy behave alike here, for exactly the reasons covered in flutter-web-clipboard-api.
The support matrix you ship into
caniuse puts the Web Share API at 94.39% global usage, but that headline hides the row that matters: Firefox has never shipped it.
| Browser / platform | Web Share API |
|---|---|
| Chrome 128+ and Edge 95+ | Supported |
| Chrome 89-127 and Edge 81-94 | Partial support |
| Safari desktop 12.1+ | Supported |
| Safari on iOS 12.2+ | Supported |
| Firefox, all versions | Not supported |
| Chrome for Android, Firefox for Android | Supported |
| Samsung Internet 8.2+ | Supported |
| Opera 114+ and Opera Mobile 80+ | Supported |
| Opera Mini, Android Browser | Not supported |
Device behavior differs too: on Windows the promise resolves as soon as the share popup launches, while on Android it resolves once the data reaches the share target. A SnackBar after await therefore means something different per platform, and a resolved future is never proof that the user shared anything.
Web Share Level 2: files
Level 2 adds a files array, and it must be tested rather than assumed. navigator.canShare({ files }) returns false when the implementation does not support file sharing, when a type is not on its shareable list, or when the browser would consider the share hostile. MDN’s shareable list includes .pdf, .png, .jpg, .webp, .svg, .mp3, .wav, .ogg, .mp4, .webm, and .txt.
const files = [new File([bytes], 'report.pdf', { type: 'application/pdf' })];
if (navigator.canShare({ files })) {
await navigator.share({ files, title: 'Monthly report' });
} else {
// No file sharing on this target: fall back to a download.
}
How share_plus surfaces this
The plugin’s web implementation is a thin, honest wrapper. It builds a ShareData payload (mapping ShareParams.subject ?? title onto the title field, and rejecting outright a call that supplies both uri and text), then runs a fixed sequence:
- call
navigator.canShare(data), and if the method is missing or returns false, go to the fallback; - call
navigator.share(data); - map
AbortError, the user dismissing the sheet, toShareResultStatus.dismissed; - map every other
DOMException, includingNotAllowedError, to the fallback.
On web a successful share returns ShareResultStatus.unavailable, because the browser will not tell you which app took the content. Only dismissed carries real signal.
The fallback chain is configurable rather than fixed:
await SharePlus.instance.share(
ShareParams(
files: [XFile('assets/report.pdf')],
text: 'Monthly report attached',
downloadFallbackEnabled: false, // files: download via <a download>, or throw
mailToFallbackEnabled: false, // text/uri: mailto: via url_launcher_web, or throw
),
);
With both fallbacks disabled, a failed share throws an Exception carrying the original reason, so wrap the call in try/catch rather than letting it escape an event handler.
Progressive enhancement: hide what cannot work
import 'dart:js_interop';
import 'package:web/web.dart' as web;
bool get _shareApiAvailable =>
web.window.navigator.hasProperty('share'.toJS);
// In build():
if (!_shareApiAvailable) return const SizedBox.shrink();
That one check covers Firefox, insecure contexts, and <iframe>s where the web-share policy blocks the API. Then give the user something that always works, such as a copy-to-clipboard button, instead of a control that quietly downloads a file.
Takeaway: Treat navigator.share as an enhancement rather than a feature: gate it behind a navigator.share feature check, call it inside the same tap, probe file sharing with canShare({ files }), expect unavailable on success and dismissed on cancel, and ship a real fallback for Firefox and every browser that never ships the API.
Sources
- https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
- https://developer.mozilla.org/en-US/docs/Web/API/Navigator/canShare
- https://pub.dev/packages/share_plus
- https://caniuse.com/web-share
- https://raw.githubusercontent.com/fluttercommunity/plus_plugins/main/packages/share_plus/share_plus/lib/src/share_plus_web.dart