Working Around Share Sheet File Size Limits
This guide is part of Sharing Files and Media Payloads, which itself is part of Native Device Integration Patterns.
There is no documented maximum for a Web Share payload, which is exactly what makes size the hardest file-sharing problem to reason about. navigator.canShare() returns true, the sheet opens, the user picks a destination — and then the transfer fails, because the limit lives in the receiving application and was never visible to your code. The workaround is not a magic number but a strategy: measure before offering, reduce what you can, and switch payload shape rather than hoping.
Feature Detection Gate
Support and size are separate questions, so answer both before drawing the button.
// payload-check.js
const SAFE_BYTES = 5_000_000; // conservative: reliable across targets
const RISKY_BYTES = 20_000_000; // frequently refused past this point
export function assessPayload(files) {
const supported =
window.isSecureContext === true &&
typeof navigator.share === 'function' &&
typeof navigator.canShare === 'function' &&
files.length > 0 &&
files.every((f) => f instanceof File);
if (!supported) return { ok: false, reason: 'unsupported', bytes: 0 };
const bytes = files.reduce((total, f) => total + f.size, 0);
const validates = (() => { try { return navigator.canShare({ files }); } catch { return false; } })();
if (!validates) return { ok: false, reason: 'type-rejected', bytes };
if (bytes > RISKY_BYTES) return { ok: false, reason: 'too-large', bytes };
return { ok: true, band: bytes > SAFE_BYTES ? 'risky' : 'safe', bytes };
}
Returning a band rather than a boolean lets the interface be honest in the middle case: between 5 and 20 MB the share usually works, so offer it — while keeping the link route one tap away.
Step 1 — Reduce Images Before They Become Files
Most oversized payloads are images exported at display resolution when the recipient only needs viewing resolution. Re-encoding through a canvas is fast and typically removes an order of magnitude.
// downscale.js
export async function downscaleImage(file, { maxEdge = 2048, type = 'image/webp', quality = 0.85 } = {}) {
const bitmap = await createImageBitmap(file);
const scale = Math.min(1, maxEdge / Math.max(bitmap.width, bitmap.height));
if (scale === 1 && file.type === type) return file; // already small enough
const canvas = new OffscreenCanvas(
Math.round(bitmap.width * scale),
Math.round(bitmap.height * scale)
);
canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height);
bitmap.close();
const blob = await canvas.convertToBlob({ type, quality });
const name = file.name.replace(/\.\w+$/, '') + '.' + type.split('/')[1];
return new File([blob], name, { type, lastModified: Date.now() });
}
bitmap.close() matters on phones — an ImageBitmap holds decoded pixel data, and a batch of them will exhaust memory long before the shares do. Where OffscreenCanvas is unavailable, the same routine works with a detached <canvas> and toBlob, as in sharing images rendered from a canvas.
Step 2 — Switch Payload Shape Above the Threshold
When reduction is not enough — a video, a scanned report, a multi-file export — change what you send rather than trying to squeeze it through.
// share-strategy.js
import { assessPayload } from './payload-check.js';
export function chooseStrategy(files, linkUrl) {
const assessment = assessPayload(files);
if (assessment.ok) {
return { mode: 'files', files, band: assessment.band };
}
if (linkUrl) {
return { mode: 'link', url: linkUrl, reason: assessment.reason };
}
return { mode: 'download', files, reason: assessment.reason };
}
export async function executeStrategy(strategy, meta = {}) {
if (strategy.mode === 'files') {
try {
await navigator.share({ ...meta, files: strategy.files });
return { outcome: 'shared' };
} catch (err) {
if (err.name === 'AbortError') return { outcome: 'dismissed' };
if (err.name === 'DataError') return { outcome: 'retry-as-link' }; // too big for the target
return { outcome: 'failed', reason: err.name };
}
}
if (strategy.mode === 'link' && typeof navigator.share === 'function') {
await navigator.share({ ...meta, url: strategy.url });
return { outcome: 'shared-link' };
}
return { outcome: 'download' };
}
The retry-as-link outcome is the recovery that turns a dead end into a working flow: the target refused the bytes, so offer the same content as a link without making the user start over. A link share is also the only route that works on browsers with no file support at all, which keeps one code path serving both problems.
Step 3 — Be Honest in the Interface
Users accept a limitation they can see and resent one that appears after they have committed. Surface the size decision on the button itself.
// size-label.js
export function shareLabel(strategy, bytes) {
const mb = (bytes / 1_000_000).toFixed(1);
if (strategy.mode === 'files' && strategy.band === 'safe') return { label: 'Share', hint: null };
if (strategy.mode === 'files' && strategy.band === 'risky') return { label: 'Share', hint: `${mb} MB — some apps may refuse it` };
if (strategy.mode === 'link') return { label: 'Share link', hint: `File is ${mb} MB — sending a link instead` };
return { label: 'Download', hint: `${mb} MB` };
}
Failure Modes and Recovery
| Symptom | Cause | Minimal fix |
|---|---|---|
DataError after the sheet opens |
Target refused the byte count | Fall back to a link share on that error |
| Share works to Mail, fails to a chat app | Per-application limits differ | Apply your own conservative threshold |
canShare true for a 40 MB video |
canShare never checks size |
Sum file.size yourself before offering |
| Phone runs out of memory during export | ImageBitmap objects not closed |
Call bitmap.close() after drawing |
| Zip rejected by the target | application/zip unsupported in many apps |
Compress the media, or link to an archive |
| Long freeze before the sheet | Re-encoding inside the click handler | Reduce ahead of the gesture |
Browser and Platform Caveat
Because the constraint lives in the receiving app rather than the browser, the same payload behaves differently on one device depending on which target the user picks — Mail and Files are typically generous, messaging apps far less so. iOS additionally holds the whole payload in memory while the modal sheet is open, so very large shares are the ones most likely to end in a discarded tab. OffscreenCanvas and convertToBlob are available in current Chrome, Edge and Safari but not universally, so keep the <canvas>-based path as a fallback. And note that a share which succeeds on a fast connection may fail on a slow one when the target uploads immediately — another argument for shipping a link when the content is genuinely large, which also keeps the offline queue small enough to survive eviction.
FAQ
What is the actual size limit for navigator.share?
The specification defines none, and no browser publishes one. In practice the limit belongs to the receiving application, so the same file succeeds to Mail and fails to a messaging app. Treat a few megabytes as reliably safe and anything past twenty as likely to fail somewhere.
Does canShare tell me the payload is too big?
No. canShare evaluates types and payload shape, not bytes, so it returns true for a file that a target will later refuse. Size must be checked by your own code before the share, and DataError handled after it.
Is zipping several files a good way to stay under a limit?
Rarely. Already-compressed media barely shrinks, so a zip of photos is roughly the size of the photos, and many share targets refuse application/zip outright. Compress the media itself, or share a link to a server-side archive.