Web Share Target: Complete Guide
The Web Share Target API is one of the most powerful yet underutilized features of modern Progressive Web Apps (PWAs). It allows your web application to receive shared content from other apps on the user's device — appearing in the native share sheet alongside installed applications. This guide walks you through everything you need to know to implement it correctly, from basic setup to advanced handling of different content types.
What Is Web Share Target?
Web Share Target is the receiving counterpart to the Web Share API. While Web Share lets your app send content to other apps, Web Share Target lets your app receive content that users share from other apps. When a user selects photos in their gallery, a link in their browser, or text in any app and taps "Share," your PWA can appear as a destination in the system share sheet.
This bridges a critical gap between web and native. Historically, only installed native apps could register as share targets. With Web Share Target, an installed PWA can do the same, making the web app feel like a first-class citizen on the operating system.
Why It Matters
- Seamless integration: Users can share content to your app without copy-pasting or switching contexts manually.
- Native-feeling UX: Your PWA behaves like a native app in the share sheet, increasing perceived quality.
- Higher engagement: Reducing friction in content ingestion leads to more frequent app usage.
- Cross-platform reach: One codebase handles sharing on Android, Chrome OS, and other supporting platforms.
- No app store required: Distribution happens through the browser and PWA installation flow.
Browser and Platform Support
Web Share Target is currently supported on Android via Chrome and Edge, and on Chrome OS. It is not supported on iOS Safari or desktop browsers as of this writing. The feature requires the PWA to be installed (added to home screen) — it will not appear in the share sheet for non-installed sites.
How It Works: The High-Level Flow
When a user shares content and selects your PWA, the browser opens your app at a URL you specify in the manifest, sending the shared data as a POST request (for files) or GET request (for text/links). Your service worker intercepts this request, extracts the shared data, and either redirects the client to a friendly URL or caches the data for the page to consume.
Step 1: Declare the Share Target in the Manifest
The share target is declared inside your manifest.json using the share_target member. Here is a minimal example that accepts text and links:
{
"name": "My Notes",
"short_name": "Notes",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3367d6",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"share_target": {
"action": "/share-target",
"method": "GET",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}
The action field is the URL the browser navigates to when your app is selected. The method is either GET or POST. The params object maps the standard share fields (title, text, url) to query parameter names your app will read.
Step 2: Handle GET-Based Sharing (Text and Links)
For text and links, a GET request is sufficient. The browser appends your params as query strings to the action URL. No service worker is strictly required — your page can read the query parameters directly:
// Inside the page loaded at /share-target
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) {
showShareDialog({ title, text, url });
}
This approach is simple and works well for note-taking apps, bookmark managers, and link shorteners. However, GET requests cannot carry files. For images, videos, or any binary content, you must use POST.
Step 3: Handle POST-Based Sharing (Files)
To receive files, set method to "POST" and add an enctype of "multipart/form-data". You also declare which file types you accept:
{
"share_target": {
"action": "/share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "images",
"accept": ["image/png", "image/jpeg", "image/webp"]
},
{
"name": "videos",
"accept": ["video/mp4"]
}
]
}
}
}
When the user shares files, the browser sends a POST request with multipart form data to your action URL. Your service worker must intercept this request because the browser will not render a POST response directly.
Step 4: Intercept the POST in the Service Worker
The service worker captures the POST request, reads the form data, stores it (typically in the Cache API or IndexedDB), and responds with a redirect to a GET URL that your page can render:
// sw.js
const SHARE_TARGET = "/share-target";
const SHARE_CACHE = "shared-data";
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (event.request.method === "POST" && url.pathname === SHARE_TARGET) {
event.respondWith(handleShare(event.request));
}
});
async function handleShare(request) {
const formData = await request.formData();
const sharedData = {
title: formData.get("title") || "",
text: formData.get("text") || "",
url: formData.get("url") || "",
files: []
};
// Extract files
const imageFiles = formData.getAll("images");
for (const file of imageFiles) {
const key = `shared-image-${Date.now()}-${file.name}`;
const cache = await caches.open(SHARE_CACHE);
await cache.put(key, new Response(file, {
headers: { "Content-Type": file.type }
}));
sharedData.files.push({ key, name: file.name, type: file.type });
}
// Store metadata for the page to read
const metaKey = `shared-meta-${Date.now()}`;
const cache = await caches.open(SHARE_CACHE);
await cache.put(metaKey, new Response(JSON.stringify(sharedData), {
headers: { "Content-Type": "application/json" }
}));
// Redirect to a GET URL with the metadata key
return Response.redirect(`/share-receive?key=${metaKey}`, 303);
}
The 303 See Other redirect is essential — it converts the POST into a subsequent GET that the browser can render as a normal page navigation.
Step 5: Consume the Shared Data in Your Page
The page at /share-receive reads the metadata key from the query string, fetches the stored data from the cache, and displays it to the user:
// Inside /share-receive page
async function loadSharedContent() {
const params = new URLSearchParams(window.location.search);
const key = params.get("key");
if (!key) return;
const cache = await caches.open("shared-data");
const metaResponse = await cache.match(key);
if (!metaResponse) return;
const data = await metaResponse.json();
displayTitle(data.title);
displayText(data.text);
for (const fileMeta of data.files) {
const fileResponse = await cache.match(fileMeta.key);
const blob = await fileResponse.blob();
const objectUrl = URL.createObjectURL(blob);
renderImage(objectUrl, fileMeta.name);
}
// Clean up cached data after consumption
await cache.delete(key);
for (const fileMeta of data.files) {
await cache.delete(fileMeta.key);
}
}
loadSharedContent();
Step 6: Using IndexedDB for Larger or Persistent Data
The Cache API works for quick handoffs, but for larger files or data you want to persist across sessions, IndexedDB is a better choice. Here is a compact helper using a small wrapper:
// db.js
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open("shared-store", 1);
req.onupgradeneeded = () => {
req.result.createObjectStore("files", { keyPath: "id" });
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function saveFile(blob, id) {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction("files", "readwrite");
tx.objectStore("files").put({ id, blob, created: Date.now() });
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}
async function getFile(id) {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction("files", "readonly");
const req = tx.objectStore("files").get(id);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
Use these helpers inside your service worker's handleShare function instead of the Cache API when you need durability or when files are large enough that cache eviction could be a problem.
Best Practices
- Always use POST for files. GET requests cannot carry file payloads and have URL length limits.
- Use 303 redirects. This is the only redirect status that reliably converts a POST into a GET across browsers.
- Clean up cached data. Shared files stored in the Cache API or IndexedDB should be deleted once consumed, or you risk filling up storage.
- Validate and sanitize input. Treat shared content as untrusted user input. Sanitize text, validate file types, and enforce size limits.
- Provide a graceful fallback. If the share target page is opened without shared data (for example, the user navigates there manually), show normal content rather than an error.
- Test with real devices. Share Target only works on installed PWAs on supported platforms. Use an Android device or emulator with Chrome for accurate testing.
- Keep the service worker simple. Do heavy processing on the client page, not in the service worker. The service worker should only receive, store, and redirect.
- Handle multiple files. Use
formData.getAll()rather thanformData.get()for file fields, since users can share multiple files at once. - Update icons properly. Maskable icons ensure your app icon looks correct in the share sheet on all Android versions.
- Consider offline behavior. Since the service worker handles the POST, sharing works even when the network is offline, as long as the service worker is active.
Common Pitfalls
One frequent issue is the share target not appearing in the share sheet. This usually means the PWA is not installed, the manifest is invalid, or the service worker is not registered. Verify installation by checking that the app appears in the app drawer, and use Chrome DevTools' Application tab to inspect the manifest and service worker status.
Another common mistake is forgetting to handle the case where the share target page is loaded directly. Always check for the presence of shared data before attempting to render it, and provide a sensible default view.
Finally, be aware that the files parameter in the manifest must match the field names you read with formData.getAll() in the service worker. A mismatch here silently results in empty file lists.
Putting It All Together
Here is a condensed checklist of the complete implementation:
// 1. manifest.json — declare share_target
// 2. sw.js — listen for POST to action URL, parse formData, store, redirect 303
// 3. /share-receive page — read stored data, render UI, clean up
// 4. Install the PWA and test from another app's share sheet
The Web Share Target API transforms a PWA from a passive website into an active participant in the operating system's content flow. By accepting text, links, and files directly from the native share sheet, your app removes friction and delivers an experience indistinguishable from a native application. With proper manifest configuration, a well-structured service worker, and thoughtful cleanup of shared data, you can build a robust sharing experience that works reliably across supported platforms. As browser support expands, implementing Web Share Target today positions your app to benefit immediately while remaining forward-compatible with the evolving web platform.