Sharing Multiple Files in One Share Call
This guide is part of Sharing Files and Media Payloads, which itself is part of Native Device Integration Patterns.
navigator.share({ files }) accepts an array, which makes multi-file sharing look like a trivial extension of the single-file case. It is not. Support is evaluated for the set: a browser that happily shares one JPEG can refuse five, a target that accepts images can refuse a batch mixing images with a PDF, and the combined size counts against a limit that no API exposes. A payload that validates on your phone can fail on the next.
Feature Detection Gate
Validate the exact array — never a single representative file.
// multi-file-support.js
export function canShareSet(files) {
if (window.isSecureContext !== true) return false;
if (typeof navigator.share !== 'function') return false;
if (typeof navigator.canShare !== 'function') return false;
if (!Array.isArray(files) || files.length === 0) return false;
if (!files.every((f) => f instanceof File)) return false;
try {
return navigator.canShare({ files }); // the SET, not files[0]
} catch {
return false;
}
}
The comment marks the mistake this whole page exists to prevent. Validating files[0] and then sharing all six is the multi-file equivalent of not validating at all.
Step 1 — Group by Type Before Building the Payload
Mixed sets fail disproportionately, and the fix is usually free: most real batches are already one kind of thing with a stray outlier.
// group-files.js
export function groupByKind(files) {
const groups = new Map();
for (const file of files) {
const kind = (file.type.split('/')[0] || 'other'); // image, video, audio, application
if (!groups.has(kind)) groups.set(kind, []);
groups.get(kind).push(file);
}
// Largest group first — it is the one most worth sharing natively.
return [...groups.values()].sort((a, b) => b.length - a.length);
}
Grouping by the MIME type rather than the full subtype is deliberate: image/png and image/jpeg together are treated as one uniform batch by every platform tested, while image/* mixed with application/pdf is the combination that gets refused.
Step 2 — Degrade the Set, Not the Feature
When the full array is rejected, shrinking it is almost always better than abandoning the share. Try progressively smaller batches before giving up.
// share-set.js
import { canShareSet } from './multi-file-support.js';
export async function shareLargestSupportedSet(files, meta = {}) {
const candidates = [
files, // everything
files.slice(0, 4), // a common platform cap
files.slice(0, 1) // one file always has the best odds
];
const payload = candidates.find((set) => set.length > 0 && canShareSet(set));
if (!payload) return { outcome: 'fallback', reason: 'no-supported-set' };
const withMeta = { ...meta, files: payload };
const finalPayload = navigator.canShare(withMeta) ? withMeta : { files: payload };
try {
await navigator.share(finalPayload);
return { outcome: 'shared', shared: payload.length, omitted: files.length - payload.length };
} catch (err) {
if (err.name === 'AbortError') return { outcome: 'dismissed' };
if (err.name === 'DataError') return { outcome: 'fallback', reason: 'target-rejected' };
return { outcome: 'fallback', reason: err.name };
}
}
Returning omitted is what makes this honest. A share that quietly sends three of twelve photos is a bug from the user’s point of view; a share that sends three and says so is a working feature with a stated limit.
Step 3 — Tell the User What Will Be Sent
The interface should reflect the negotiated payload before the tap, not after.
// set-label.js
import { canShareSet } from './multi-file-support.js';
export function describeSet(files) {
const total = files.reduce((sum, f) => sum + f.size, 0);
const mb = (total / 1_000_000).toFixed(1);
if (canShareSet(files)) {
return { label: `Share ${files.length} files`, hint: `${mb} MB` };
}
if (files.length > 1 && canShareSet(files.slice(0, 1))) {
return { label: 'Share first file', hint: `${files.length - 1} more will be omitted` };
}
return { label: 'Share link', hint: 'Attachments are not supported here' };
}
Because canShareSet is synchronous, this can run on every render — no caching, no stale answers, and the label always matches what pressing it will do.
Step 4 — Encode Order into Filenames
Array position does not survive the trip. Receiving apps sort by name, by type, or by arrival, so a sequence that matters must carry its own ordering.
// ordered-names.js
export function withOrdinalNames(files, base = 'page') {
const width = String(files.length).length; // 9 files → 1, 12 files → 2
return files.map((file, i) => {
const ext = file.name.split('.').pop();
const index = String(i + 1).padStart(width, '0');
return new File([file], `${base}-${index}.${ext}`, { type: file.type, lastModified: file.lastModified });
});
}
Zero-padding is the detail that makes this work: page-10 sorts before page-2 in a plain lexical sort, which is exactly what most file browsers do.
Failure Modes and Recovery
| Symptom | Cause | Minimal fix |
|---|---|---|
| One file shares, the batch fails | Validated files[0] instead of the set |
Pass the whole array to canShare |
| Mixed images and PDF refused | Heterogeneous payload | Group by MIME type and share the largest group |
DataError after the sheet opens |
Combined size over the target’s limit | Retry with a smaller batch, then a link |
| Files arrive out of order | Array order is not preserved | Encode ordinals in zero-padded filenames |
| Only some files arrive | Target silently capped the batch | Report omitted in the UI; do not claim success |
| Slow, janky share on a phone | All files re-encoded in the handler | Prepare the set before the gesture |
Browser and Platform Caveat
Chrome on Android has the most generous multi-file behaviour, routinely accepting uniform batches of a dozen images. iOS and iPadOS accept multi-file payloads well for images and PDFs but are stricter about mixed sets, and the modal sheet holds the whole batch in memory while it is open — a real constraint for large photo selections. Desktop Safari is narrower again, and Firefox has no share implementation at all, so the link route serves a meaningful slice of desktop users rather than being an edge case. Because none of these limits is published, the negotiation pattern above is not defensive over-engineering: it is the only way to find the boundary without asking the user to discover it for you. When even a single file cannot go, fall back to the copy-to-clipboard patterns so the control still does something useful.
FAQ
Why does canShare accept one file but reject three of the same type?
Support is evaluated for the payload as a whole, and some platforms cap the number of files or the combined size regardless of type. Validate the exact array you intend to send and fall back to a smaller batch when the full set is refused.
Can I mix images and PDFs in one share?
You can construct the payload, but heterogeneous sets are rejected much more often than uniform ones, both by browsers and by receiving apps that expect a single content kind. Group by type and share the largest supported group, or share a link to a collection.
Do the files arrive in the order I put them in the array?
Order is not guaranteed once the payload reaches the receiving application, which may sort by name, by type, or arbitrarily. If sequence carries meaning — pages of a document, frames of a sequence — encode it in the filenames rather than relying on array position.