← Back to DevBytes

Web Share Target: Complete Guide

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

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles