Registering a Web Share Target in the Manifest

This guide is part of Web Share Target API.

A share target is the inverse of navigator.share: instead of your app pushing content out to other apps, it registers itself as a destination in the operating system’s native share sheet so that other apps can push content in. That registration lives entirely in the web app manifest, in a single member named share_target. Get its four fields — action, method, enctype, and params — correct, and an installed Progressive Web App receives shared titles, text, URLs, and files exactly like a native app. Get one field wrong, and the app either never appears in the share sheet or receives an empty request.

This guide covers the share_target member end to end: the meaning of each field, when GET and POST apply, how enctype interacts with file sharing, how the incoming request maps back to your params, the installability prerequisites, and how to verify the whole chain works on a real device.

share_target manifest fields mapped to the incoming request Diagram showing the share_target object with action, method, enctype and params on the left, branching into a GET path that produces a query string read with URLSearchParams and a POST path that produces multipart form-data intercepted by the service worker. manifest.json "share_target": { action: "/share" method: GET | POST enctype: ... params: { title, text, url, files } } method? GET navigation /share?title=..&text=.. &url=.. read in the page with new URLSearchParams( location.search) GET POST request multipart/form-data title, text, url, files[] intercepted by the service worker fetch handler, await request.formData() POST

Problem Framing

Without a share_target declaration, your installed web app is invisible to the rest of the device. A user who taps “Share” on a photo, a link, or a highlighted passage of text sees a sheet full of native apps and other installed PWAs — but not yours. There is no JavaScript API that can add your app to that sheet at runtime; the operating system builds the list from the manifests it read at install time. Registration is declarative and one-time, and it is the only way in.

The failure surface has two distinct halves. The first is appearing in the share sheet at all: this depends on installability, a secure context, and a syntactically valid share_target member. The second is correctly receiving the payload once the user picks your app: this depends on matching the method, enctype, and params to how you actually read the incoming request. A target can appear perfectly in the sheet and still hand your landing page an empty payload if, for example, you declared a GET method but tried to read files, or declared POST but never registered a service worker to intercept the request.

This registration is the entry point to the wider Web Share Target API. Once content arrives, the downstream work — receiving shared files and text and routing it into your UI — depends on getting the manifest right first.

Prerequisites Checklist

Before adding share_target, confirm each of these. A share target that fails any installability requirement will never register, and the failure is silent — the app simply does not appear in the sheet.

Browser Support Snapshot

Browser / Platform share_target GET share_target POST Files via POST
Chrome for Android 76+ ✅ (89+) ✅ (89+)
Edge for Android 79+
Samsung Internet 12+
Chrome Desktop (Win/Mac/Chrome OS) ⚠️ Chrome OS only ⚠️ Chrome OS only ⚠️ Chrome OS only
Firefox for Android (all)
Firefox Desktop (all)
Safari iOS / iPadOS (all)
Safari macOS (all)

The practical takeaway: the Web Share Target API is an Android-Chromium feature with partial Chrome OS desktop support. Treat it as a progressive enhancement layered on top of an experience that already works without it. Users on iOS and Firefox will never reach your action route through a share, so that route must also make sense as a normal navigation destination.

Step-by-Step Implementation

Step 1 — Add the share_target member to an existing manifest

Open your web app manifest and add a share_target member alongside the existing keys. This example registers a GET target that receives a title, text, and URL — the simplest case, sufficient for sharing links and plain text.

{
  "name": "Clip Vault",
  "short_name": "ClipVault",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "icons": [
    { "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
  ],
  "share_target": {
    "action": "/share",
    "method": "GET",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url"
    }
  }
}

The params object is a mapping, not a list. Each key (title, text, url) is a fixed name defined by the specification — the field the OS knows how to fill. Each value ("title", "text", "url") is the query-parameter name you choose to receive it under. They are identical here for clarity, but you could map "text" to a value of "note" and then read ?note= on your landing page.

Step 2 — Choose the method and enctype

The method determines how the payload arrives. Use GET when you only need title, text, and url; the values arrive as a query string on a normal navigation. Use POST — with enctype set to multipart/form-data — whenever you need to receive files, because binary data cannot be carried in a URL. The decision, its edge cases, and the exact manifest for each are covered in depth in handling GET vs POST share targets.

{
  "share_target": {
    "action": "/share",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [
        {
          "name": "media",
          "accept": ["image/*", "video/*"]
        }
      ]
    }
  }
}

enctype has two valid values. application/x-www-form-urlencoded is the default and is used for text-only POST targets. multipart/form-data is required the moment params.files is present — it is the only encoding that can carry file bytes. Declaring files without setting enctype to multipart/form-data causes the shared files to be dropped.

Step 3 — Map the params, including files and accept

The files value inside params is an array of objects, each with a name (the form-field name you will read) and an accept array of MIME types or extensions. The accept list does double duty: it tells the OS which apps to offer for a given file type, so your app only appears in the share sheet when the shared content matches. Narrowing accept and validating types on receipt is detailed in filtering shared file types with MIME accept.

You can declare multiple file param entries to receive different categories under different field names:

{
  "params": {
    "title": "title",
    "text": "text",
    "url": "url",
    "files": [
      { "name": "images", "accept": ["image/jpeg", "image/png", "image/webp"] },
      { "name": "documents", "accept": [".pdf", "text/plain"] }
    ]
  }
}

Step 4 — Handle the landing route

How you read the payload depends entirely on the method. For a GET target, the browser performs an ordinary navigation to action with the values appended as a query string, so you read them in page JavaScript with URLSearchParams after confirming you are in a secure context:

// share-landing.js — GET target: read query params on the /share page
export function readSharedFromQuery() {
  if (!window.isSecureContext) {
    return null;
  }
  const params = new URLSearchParams(window.location.search);
  const title = params.get('title') ?? '';
  const text = params.get('text') ?? '';
  const url = params.get('url') ?? '';

  if (!title && !text && !url) {
    return null; // navigated to /share directly, not via a share
  }
  return { title, text, url };
}

For a POST target the request never touches your server. Instead, register a service worker fetch handler that matches the action path and reads the multipart body with request.formData(), then redirects the user to a page that can render the result:

// sw.js — POST target: intercept the share request
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (event.request.method === 'POST' && url.pathname === '/share') {
    event.respondWith(handleSharedPost(event.request));
  }
});

async function handleSharedPost(request) {
  const formData = await request.formData();
  const title = formData.get('title') ?? '';
  const text = formData.get('text') ?? '';
  const sharedUrl = formData.get('url') ?? '';
  const files = formData.getAll('media'); // matches params.files[].name

  await stashSharedPayload({ title, text, url: sharedUrl, files });
  // Redirect to a GET route the page can render normally
  return Response.redirect('/share?received=1', 303);
}

Parsing that multipart body robustly — and stashing files so the redirected page can pick them up — is a topic in its own right, covered in receiving shared files and text.

Step 5 — Verify the installation registers the target

A manifest change only takes effect after the app is reinstalled or the manifest is re-read. In development, open DevTools, go to Application → Manifest, and confirm the share_target section renders without warnings. Then trigger an install (the address-bar install icon or the three-dot menu → “Install app”), and finally open another app, share content, and confirm your app appears in the sheet. The full verification routine is in the Testing section below.

Payload Validation and Error Boundary

Never trust an incoming share payload. The values come from arbitrary source apps and may be empty, truncated, or of an unexpected type. For file targets, validate each received file against the MIME types you actually support before processing, mirroring the pre-flight navigator.canShare() check you would run on the outbound side:

// validate-shared.js — reject files whose type you cannot handle
const SUPPORTED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);

export function partitionSharedFiles(files) {
  const accepted = [];
  const rejected = [];
  for (const file of files) {
    if (file instanceof File && SUPPORTED_TYPES.has(file.type)) {
      accepted.push(file);
    } else {
      rejected.push(file);
    }
  }
  return { accepted, rejected };
}

The most common error boundaries when registering a share target:

Symptom Cause Correct response
App never appears in the share sheet App not installed, or manifest fails installability Reinstall; fix icons/display/HTTPS
App appears but landing page is empty params names do not match how the page reads them Align params values with URLSearchParams/formData keys
Shared files are missing GET method used, or enctype not multipart/form-data Switch to POST with multipart/form-data
action URL returns 404 on share POST target with no service worker fetch handler Add a handler that matches the path and calls respondWith
App absent for a given file type Shared type not in any accept list Widen accept or add a matching file param entry

Platform Gotchas

Android Chrome, Edge, and Samsung Internet. These are the primary supported targets. GET targets have worked since Chrome 76; POST and file sharing arrived in Chrome 89. Edge and Samsung Internet track Chromium closely and behave identically. The app must be genuinely installed (added to the home screen via the install prompt), not merely bookmarked.

iOS Safari and iPadOS. No support at any version as of mid-2026. Installed PWAs on iOS do not register as share destinations, and there is no flag or entitlement to enable it. Design the incoming-share feature as an enhancement: iOS users should still be able to paste a link or upload a file through your normal UI.

Firefox, all platforms. Firefox implements the outbound navigator.share on Android but does not implement the Web Share Target API on any platform. Do not rely on it as a receiving destination in Firefox.

Desktop. On Chrome OS, installed PWAs can register as share targets and appear in the Chrome OS share flow. On Windows and macOS, desktop Chrome does not currently surface web share targets in the OS share UI, so treat desktop as unsupported outside Chrome OS.

Scope traps. If action sits outside the manifest scope, the registration is rejected silently. Keep action within scope, and remember that changing scope or action requires a reinstall to take effect.

Testing and Verification

DevTools checklist (Chrome on Android via remote debugging, or Chrome OS):

  1. Application → Manifest: confirm the panel lists your share_target with the correct action, method, enctype, and params. Resolve any red installability warnings first — an uninstallable app can never be a target.
  2. Application → Service Workers: confirm a worker is activated and that its source contains a fetch handler matching your action path (POST targets only).
  3. Visit chrome://apps (desktop) or the home screen (Android) and confirm the app is present as an installed app, not just a tab.
  4. Console: run window.isSecureContext on the action page and confirm it returns true.

Physical device checklist (Android):

Regression note: after every manifest edit, bump anything cache-busting the manifest itself and force a reinstall. A surprising share-target bug is simply testing against a stale, cached manifest from a previous install.

FAQ

Does the app need to be installed for the share target to appear?

Yes. The share_target member only takes effect once the Progressive Web App is installed. The operating system reads the manifest at install time and registers the app in the native share sheet. An uninstalled site — even one that meets every installability criterion — never appears as a share destination, and there is no runtime API to add it.

Why does my action URL 404 when someone shares to my app?

For a POST share target, the action URL is a request that is meant to be intercepted, not served from the network. You must register a service worker fetch handler that matches the action path and calls event.respondWith(...). Without it, the browser attempts a real POST navigation and your server returns 404 because no server-side route handles that method and path.

Can iOS Safari receive shared content through a share_target?

No. As of mid-2026 neither iOS Safari nor any other iOS browser supports the Web Share Target API, and installed PWAs on iOS do not appear in the system share sheet as destinations. Only Chromium-based Android browsers — Chrome, Edge, and Samsung Internet — implement it, with partial support on Chrome OS. Always provide a non-target fallback for iOS.

What is the difference between action and start_url?

start_url is where the app opens on a normal launch from its icon. action is the URL the OS navigates to specifically when content is shared into the app. They may be the same value, but using a dedicated action route lets you cleanly separate a cold launch from an incoming share without inspecting the query string on every startup.