# share_target manifest member

> The share_target member registers an installed PWA in the OS share sheet and delivers shared text, URLs, and files to an in-scope URL by GET or multipart POST.

import Figure from '@components/Figure.astro';
import shareTargetDiagram from '@assets/diagrams/share-target.svg';

`share_target` registers an installed web app as a destination in the operating system's share
sheet. When the user picks the app, the browser delivers the shared title, text, URL, and files to
a URL inside the app's scope, either as query parameters on a GET navigation or as a
`multipart/form-data` POST that a service worker can intercept.

Chrome 76 on Android, Edge 89 on desktop (GET, with limited file support), and Samsung Internet
12.0 read the member (BCD `html.manifest.share_target`). Safari 27 on iOS and macOS and Firefox
157 do not implement it, so the app never appears in their share sheets. The member is the
receiving half of sharing; the sending half is `navigator.share()`.

<Figure src={shareTargetDiagram} alt="Flow diagram of a share target: the manifest declares share_target with action, method, enctype and params; after the app is installed the OS share sheet lists it. When the user picks it the browser either navigates to the action URL with title, text and url as query parameters (GET), or submits multipart/form-data that the service worker fetch handler reads with formData(), stores, and answers with a 303 redirect (POST). Both paths end on a page showing the shared content." caption="Share target: from the manifest declaration through the OS share sheet to the GET or POST handler." />

## Member

- **Type**: object with `action` (in-scope URL that receives the share), `method` (`"GET"` or
  `"POST"`), `enctype` (`"application/x-www-form-urlencoded"` or `"multipart/form-data"`), and
  `params`, an object mapping the shared fields to parameter names: `title`, `text`, `url`, and
  `files`, an array of `{ "name", "accept" }` where `accept` lists MIME types or extensions.
- **Default**: absent, so the app is not a share target. Inside the object `method` defaults to
  `"GET"` and `enctype` to `"application/x-www-form-urlencoded"`.
- **Example value**: `{ "action": "/share", "method": "GET", "params": { "title": "title",
  "text": "text", "url": "url" } }`.

Chromium validates the combination and drops the whole member on a bad one, logging to the
manifest's error list: `invalid method. Allowed methods are: GET and POST.`, `invalid enctype for
GET method. Only application/x-www-form-urlencoded is supported.`, `files are only supported with
multipart/form-data POST.`, and `property 'share_target' ignored. Property 'action' is invalid.`
when `action` is cross-origin or outside `scope`. Files therefore require `"method": "POST"` with
`"enctype": "multipart/form-data"`; text-only targets work with the defaults.

The app must be installed before it appears in the share sheet, and the entry carries the
manifest's `short_name` and icon. Android shows it alongside native apps; a user who has not
installed the PWA has no way to share into it, which is why a share-target page also needs an
ordinary route for pasted content. When the shared payload has a `text` but no `url`, Chrome on
Android often places the link inside `text`, so handlers parse both fields.

:::observed
Chrome 155 on macOS 26 (English UI), DevTools > Application > Manifest, with
`"share_target": { "action": "/share", "method": "GET", "params": { "files": [{ "name": "f",
"accept": ["image/*"] }] } }`: the **Errors and warnings** section reads `files are only supported
with multipart/form-data POST.` and the manifest is treated as having no share target. Switching
to `"method": "POST", "enctype": "multipart/form-data"` clears the line.
:::

## Examples

The examples follow the two delivery paths and then cover the user who cannot reach either.

### Receiving a link and text with a GET navigation

A read-later app accepts the three text fields. The share opens
`/share?title=…&text=…&url=…` in the installed app, and the page reads the query string on load.
Because Android sometimes delivers the link in `text`, the handler falls back to the first URL it
finds there.

```json
{
  "share_target": {
    "action": "/share",
    "method": "GET",
    "params": { "title": "title", "text": "text", "url": "url" }
  }
}
```

```js
const params = new URLSearchParams(location.search);
const text = params.get('text') ?? '';
const url = params.get('url') ?? text.match(/https?:\/\/\S+/)?.[0] ?? null;

if (url) {
  saveLink({ title: params.get('title') ?? '', url });
} else {
  showPasteForm(text); // nothing shareable arrived: let the user paste a link instead
}
```

Use `history.replaceState` after saving so a reload does not store the same link twice.

### Receiving files through the service worker

Images need POST and multipart encoding. The service worker intercepts the POST to `action`, reads
the form data, stores the files, and answers with a 303 redirect so the client lands on a normal GET
page; without the redirect a reload would resubmit the share.

```json
{
  "share_target": {
    "action": "/share/upload",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "files": [{ "name": "photos", "accept": ["image/jpeg", "image/png", "image/webp"] }]
    }
  }
}
```

```js
// service-worker.js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (event.request.method !== 'POST' || url.pathname !== '/share/upload') return;

  event.respondWith((async () => {
    const form = await event.request.formData();
    const files = form.getAll('photos');
    const cache = await caches.open('shared-inbox');
    await Promise.all(files.map((file, i) => cache.put(`/shared/${Date.now()}-${i}`, new Response(file))));
    return Response.redirect('/share/review', 303);
  })());
});
```

The `/share/review` page lists `caches.open('shared-inbox')` and lets the user confirm before the
files go to the server, which also covers a share that arrives while the device is offline.

### Falling back when the app is not installed

The share sheet only lists installed apps, and Safari 27 and Firefox 157 list no web apps at all. A route that
accepts pasted content and, where `navigator.share` exists, offers the outgoing direction keeps the
feature usable everywhere.

```js
const canReceive = matchMedia('(display-mode: standalone)').matches; // installed: the OS can share into us
const canSend = 'share' in navigator;

document.querySelector('#hint').textContent = canReceive
  ? 'Share from any app and pick this one.'
  : canSend
    ? 'Install the app to receive shares; you can still share out with the button.'
    : 'Paste a link or text in the box.';
```

`display-mode: standalone` is a proxy for "installed", not a guarantee that the share target was
registered; it is the best signal a page has.

## See also

- [Receive shared content (share target)](/guides/share-target/)
- [Web Share API: native share sheet from the browser](/reference/capabilities/web-share/)
- [scope manifest member](/reference/manifest/scope/)
- [file_handlers manifest member](/reference/manifest/file-handlers/)
- [Web Share Target API](https://w3c.github.io/web-share-target/) (w3c.github.io)
- [Receiving shared data with the Web Share Target API](https://developer.chrome.com/docs/capabilities/web-apis/web-share-target) (developer.chrome.com)