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.

How Payload Sets Are Judged Three candidate sets. A small uniform batch of images is widely accepted. A large uniform batch is accepted by some platforms and refused by others on count or combined size. A mixed set of images and a PDF is the most frequently rejected shape of all. 3 × image/jpeg · 1.8 MB total uniform type, small batch, modest size — accepted almost everywhere 12 × image/jpeg · 26 MB total uniform but large — refused on count or combined size by many targets 2 × image/png + 1 × application/pdf heterogeneous — the most commonly rejected shape, even when small

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.

Negotiating Down to a Supported Set Four attempts in sequence. The full array is tried first, then a four-file batch, then a single file, and finally a link share. Each step is validated with canShare before it is offered, and the interface reports how many files were omitted. all 12 files canShare → false too many / too large first 4 files canShare → true 8 omitted, reported share() sheet opens 4 files delivered DataError target refused retry as link single file — the highest-odds native payload link share — works on every browser, no size Every step validated before it is offered — the button never promises more than it delivers

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.

Reporting a Partial Batch When only part of a selection can be shared, silently sending a subset reads as data loss. Naming the number sent and the number omitted turns the same outcome into a working feature with a stated limit. silent subset user selects 12, 4 are sent confirmation says "Shared" 8 photos are simply missing discovered by the recipient, not the sender counted and named "Shared 4 of 12 photos" the remaining 8 stay selected a second share sends the rest a limit the user can work with

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.