← Back to DevBytes

Implementing Web Share Target in Modern Web Applications

What Is Web Share Target?

Web Share Target is a specification that allows your Progressive Web App (PWA) to appear as a share destination when a user invokes the native share dialog on their device. In practical terms, when someone taps the share button in their browser, photos app, or any other application that uses the system share sheet, your PWA can show up right alongside native apps like Messages, WhatsApp, or Notes.

This is the receiving counterpart to the Web Share API, which lets your web app send data to other apps. Together, these two APIs complete the sharing ecosystem on the web, enabling web applications to participate in the same sharing workflows that were previously exclusive to native mobile and desktop applications.

Why Web Share Target Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before Web Share Target, web applications were largely invisible to the operating system's sharing infrastructure. A user browsing a photo gallery web app couldn't simply tap "share" and send an image directly to their favorite web-based image editor. They'd have to download the file, navigate to the editor, and upload it manually — a friction-filled experience that drove users toward native alternatives.

Implementing Web Share Target brings several concrete benefits:

How Web Share Target Works

The system has two complementary halves. Understanding both is crucial for a complete implementation.

The Web Share API (Sending Side)

The Web Share API allows your web app to invoke the system share dialog and send data out to other apps. It's a simple, promise-based API that accepts a share descriptor object:

// Sharing text and a URL from your web app
await navigator.share({
  title: 'Check out this article',
  text: 'I found this great tutorial on web development',
  url: 'https://example.com/article'
});

This API is widely supported across modern browsers. But sending is only half the story — receiving shares is where Web Share Target comes in.

The Web Share Target API (Receiving Side)

Web Share Target is not a JavaScript API you call directly. Instead, it's a declarative configuration you add to your web app manifest, combined with service worker logic to handle incoming shared data. Here's the flow:

  1. You declare a share_target member in your web app manifest, specifying the URL endpoint and the data parameters your app accepts
  2. When your PWA is installed, the browser registers this share target with the operating system
  3. When a user shares content to your app from anywhere on the system, the browser sends a POST (or GET) request to your specified endpoint
  4. Your service worker intercepts this request, extracts the shared data, and passes it to your application's frontend for processing

Implementing a Web Share Target

Let's walk through a complete, production-ready implementation step by step. We'll build a simple note-taking PWA that can receive shared URLs and text from other applications.

1. The Web App Manifest

First, configure your manifest with the share_target member. This tells the browser what types of shared data your app can handle and how to deliver it.

{
  "name": "Quick Notes",
  "short_name": "Notes",
  "start_url": "/index.html",
  "display": "standalone",
  "theme_color": "#4a90d9",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "share_target": {
    "action": "/share-handler",
    "method": "POST",
    "enctype": "application/x-www-form-urlencoded",
    "params": {
      "title": "name",
      "text": "description",
      "url": "link"
    }
  }
}

Let's break down the share_target object:

In this configuration, when someone shares a webpage to our app, the browser will POST to /share-handler with form fields name, description, and link containing the shared title, text, and URL respectively.

2. Handling Incoming Shares in Your Service Worker

The service worker is the gatekeeper for incoming share requests. You need to intercept the POST request to your action URL, extract the data, and then redirect the user to the actual UI page with the shared content passed along.

Here's a robust service worker implementation:

// service-worker.js

const SHARE_HANDLER_URL = '/share-handler';
const APP_SCOPE = '/';

self.addEventListener('install', (event) => {
  // Force the waiting service worker to become active
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  // Take control of all clients immediately
  event.waitUntil(clients.claim());
});

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  // Only intercept requests to our share handler endpoint
  if (url.pathname === SHARE_HANDLER_URL && event.request.method === 'POST') {
    event.respondWith(handleShareTarget(event.request));
  }
});

async function handleShareTarget(request) {
  // Parse the form data from the incoming POST request
  const formData = await request.formData();
  
  // Extract the shared fields using the param names from our manifest
  const sharedTitle = formData.get('name') || '';
  const sharedText = formData.get('description') || '';
  const sharedUrl = formData.get('link') || '';
  
  // Store the shared data temporarily (we'll retrieve it on the client side)
  // We use the service worker's messaging capability or a simple URL redirect
  // with encoded parameters to pass data to the app
  
  // Build the redirect URL with shared data as query parameters
  // This is a common pattern: redirect to your app's main page
  // and pass the shared data in the URL for the client to process
  const redirectUrl = new URL('/new-note', self.location.origin);
  
  if (sharedUrl) {
    redirectUrl.searchParams.set('sharedUrl', sharedUrl);
  }
  if (sharedTitle) {
    redirectUrl.searchParams.set('sharedTitle', sharedTitle);
  }
  if (sharedText) {
    redirectUrl.searchParams.set('sharedText', sharedText);
  }
  
  // Redirect the user to the app page with the shared data
  // Use a 302 redirect so the browser navigates to the new page
  return Response.redirect(redirectUrl.toString(), 302);
}

Important notes about this approach:

3. Processing Shared Data in Your App

Now on the client side, you need to read the query parameters and display or process the shared content. Here's how to do it on the page that receives the redirect:

// new-note.js — runs on /new-note page

document.addEventListener('DOMContentLoaded', () => {
  const urlParams = new URLSearchParams(window.location.search);
  
  const sharedUrl = urlParams.get('sharedUrl');
  const sharedTitle = urlParams.get('sharedTitle');
  const sharedText = urlParams.get('sharedText');
  
  if (sharedUrl || sharedTitle || sharedText) {
    // We received shared data — populate the note form
    populateNoteFromShare(sharedUrl, sharedTitle, sharedText);
    
    // Clean up the URL so the user doesn't see the query parameters
    // and refreshing the page doesn't re-process old share data
    window.history.replaceState({}, document.title, '/new-note');
  }
});

function populateNoteFromShare(url, title, text) {
  const titleInput = document.getElementById('note-title');
  const bodyInput = document.getElementById('note-body');
  
  // Pre-fill the form with shared data
  if (title && titleInput) {
    titleInput.value = title;
  }
  if (url && bodyInput) {
    // Append the URL to the body text
    bodyInput.value = (text ? text + '\n\n' : '') + url;
  } else if (text && bodyInput) {
    bodyInput.value = text;
  }
  
  // Show a subtle notification that content was received
  showToast('Content shared successfully!');
}

function showToast(message) {
  const toast = document.createElement('div');
  toast.className = 'toast-notification';
  toast.textContent = message;
  toast.setAttribute('role', 'status');
  toast.setAttribute('aria-live', 'polite');
  document.body.appendChild(toast);
  
  setTimeout(() => {
    toast.classList.add('fade-out');
    setTimeout(() => toast.remove(), 300);
  }, 2500);
}

The key pattern here is reading the query parameters on page load, using the shared data, and then immediately cleaning the URL with history.replaceState to prevent stale share data from being re-processed on page refresh.

4. Full Code Example: Share-Ready Note App

Let's put everything together into a complete, working example. This includes the HTML page, the service worker, and the manifest — all the pieces needed for a functional Web Share Target implementation.

File: manifest.json

{
  "name": "Quick Notes - Share Ready",
  "short_name": "Notes",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#4a90d9",
  "background_color": "#ffffff",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "share_target": {
    "action": "/share-handler",
    "method": "POST",
    "enctype": "application/x-www-form-urlencoded",
    "params": {
      "title": "name",
      "text": "description",
      "url": "link"
    }
  }
}

File: service-worker.js

// Service Worker for Quick Notes PWA
// Handles installation, caching, and Web Share Target requests

const CACHE_NAME = 'quick-notes-v2';
const SHARE_HANDLER_PATH = '/share-handler';

// Files to cache for offline use
const PRECACHE_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/new-note.js',
  '/icons/icon-192.png',
  '/icons/icon-512.png'
];

self.addEventListener('install', (event) => {
  console.log('[SW] Install event');
  
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(PRECACHE_ASSETS))
      .then(() => self.skipWaiting())
  );
});

self.addEventListener('activate', (event) => {
  console.log('[SW] Activate event');
  
  event.waitUntil(
    Promise.all([
      // Take control of all clients
      clients.claim(),
      // Clean up old caches
      caches.keys().then((cacheNames) => {
        return Promise.all(
          cacheNames
            .filter((name) => name !== CACHE_NAME)
            .map((name) => caches.delete(name))
        );
      })
    ])
  );
});

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  // Handle Web Share Target requests
  if (url.pathname === SHARE_HANDLER_PATH && event.request.method === 'POST') {
    console.log('[SW] Intercepted share target request');
    event.respondWith(handleShareTarget(event.request));
    return;
  }
  
  // For all other requests, use cache-first strategy
  event.respondWith(
    caches.match(event.request)
      .then((cachedResponse) => {
        if (cachedResponse) {
          return cachedResponse;
        }
        return fetch(event.request).then((networkResponse) => {
          // Cache new assets for future offline use
          if (networkResponse.ok && event.request.method === 'GET') {
            const responseClone = networkResponse.clone();
            caches.open(CACHE_NAME).then((cache) => {
              cache.put(event.request, responseClone);
            });
          }
          return networkResponse;
        });
      })
  );
});

async function handleShareTarget(request) {
  try {
    const formData = await request.formData();
    
    const sharedTitle = formData.get('name') || '';
    const sharedText = formData.get('description') || '';
    const sharedUrl = formData.get('link') || '';
    
    console.log('[SW] Received share:', { sharedTitle, sharedText, sharedUrl });
    
    // Build redirect URL with shared data
    const redirectUrl = new URL('/new-note', self.location.origin);
    
    if (sharedUrl) {
      redirectUrl.searchParams.set('sharedUrl', sharedUrl);
    }
    if (sharedTitle) {
      redirectUrl.searchParams.set('sharedTitle', sharedTitle);
    }
    if (sharedText) {
      redirectUrl.searchParams.set('sharedText', sharedText);
    }
    
    // Redirect to the app page
    return Response.redirect(redirectUrl.toString(), 302);
    
  } catch (error) {
    console.error('[SW] Error handling share:', error);
    // Fallback: redirect to the app without share data
    return Response.redirect('/new-note', 302);
  }
}

File: index.html (main page with install prompt)




  
  Quick Notes
  
  
  

  

Quick Notes

Your share-ready note-taking app

No notes yet. Share content from other apps to create notes!

+ New Note

File: new-note.html (the page that receives shared content)




  
  New Note - Quick Notes
  
  
  

  

New Note

← Back to notes

File: app.js (service worker registration and install prompt)

// Register the service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then((registration) => {
        console.log('Service Worker registered:', registration.scope);
      })
      .catch((error) => {
        console.error('Service Worker registration failed:', error);
      });
  });
}

// Handle PWA install prompt
let installPromptEvent = null;
const installPrompt = document.getElementById('install-prompt');
const installButton = document.getElementById('install-button');
const dismissButton = document.getElementById('dismiss-button');

window.addEventListener('beforeinstallprompt', (event) => {
  // Prevent the default browser install prompt
  event.preventDefault();
  installPromptEvent = event;
  
  // Show our custom install prompt
  installPrompt.classList.remove('hidden');
});

installButton.addEventListener('click', () => {
  if (installPromptEvent) {
    installPromptEvent.prompt();
    
    installPromptEvent.userChoice.then((choiceResult) => {
      if (choiceResult.outcome === 'accepted') {
        console.log('User installed the PWA');
      }
      installPromptEvent = null;
      installPrompt.classList.add('hidden');
    });
  }
});

dismissButton.addEventListener('click', () => {
  installPrompt.classList.add('hidden');
});

// Detect if the app is running in standalone mode (already installed)
if (window.matchMedia('(display-mode: standalone)').matches) {
  console.log('App is running in standalone mode — share target is active');
}

This complete example gives you a fully functional PWA that can receive shared URLs and text from any application on the user's device. Once installed, the app appears in the system share sheet, and shared content flows seamlessly into the note creation form.

Supported Share Data Types

Web Share Target supports three categories of incoming data, each requiring slightly different manifest configurations.

URL Sharing

When a user shares a URL (e.g., from their browser's share menu), your app receives the page title and URL. This is the most common sharing scenario:

"share_target": {
  "action": "/share-handler",
  "method": "POST",
  "enctype": "application/x-www-form-urlencoded",
  "params": {
    "title": "name",
    "url": "link"
  }
}

Text Sharing

When users share selected text or clipboard content, your app receives the text body and optionally a title:

"share_target": {
  "action": "/share-handler",
  "method": "POST",
  "enctype": "application/x-www-form-urlencoded",
  "params": {
    "title": "name",
    "text": "description"
  }
}

File Sharing

File sharing requires using "multipart/form-data" encoding. This allows your PWA to receive actual file blobs — images, documents, videos — shared from other applications. The manifest configuration changes significantly:

"share_target": {
  "action": "/share-handler",
  "method": "POST",
  "enctype": "multipart/form-data",
  "params": {
    "title": "name",
    "text": "description",
    "url": "link",
    "files": [
      {
        "name": "sharedFiles",
        "accept": ["image/*", "application/pdf", "text/*"]
      }
    ]
  }
}

When handling file shares in your service worker, you access the files through the form data:

async function handleShareTarget(request) {
  const formData = await request.formData();
  
  const sharedTitle = formData.get('name') || '';
  const sharedText = formData.get('description') || '';
  const sharedUrl = formData.get('link') || '';
  
  // Files are retrieved as File objects from the form data
  const files = formData.getAll('sharedFiles');
  
  // You can now process these files — read their contents,
  // store them in IndexedDB, or pass references to the client
  for (const file of files) {
    console.log(`Received file: ${file.name} (${file.size} bytes, type: ${file.type})`);
    
    // Example: read an image file and pass it to the client via Cache
    if (file.type.startsWith('image/')) {
      const blob = await file.arrayBuffer();
      // Store in Cache API for retrieval by the client page
      const cache = await caches.open('shared-files');
      await cache.put(
        new Request(`/shared-files/${file.name}`),
        new Response(blob, {
          headers: { 'Content-Type': file.type }
        })
      );
    }
  }
  
  // Redirect to the app with file metadata in query params
  const redirectUrl = new URL('/process-files', self.location.origin);
  redirectUrl.searchParams.set('fileCount', files.length.toString());
  redirectUrl.searchParams.set('sharedTitle', sharedTitle);
  
  return Response.redirect(redirectUrl.toString(), 302);
}

The accept array in the manifest's file parameter is a hint to the operating system about which file types your app can handle. The OS may filter the share sheet or show a warning if incompatible files are shared.

Best Practices

After implementing Web Share Target across several production PWAs, these practices consistently lead to robust, user-friendly experiences:

  • Always use POST, not GET — POST keeps shared data out of the browser's history stack and avoids URL length limitations. GET-based share targets are simpler but should only be used for very short URL-only shares where you want bookmarkability.
  • Clean the URL after processing — use history.replaceState to remove query parameters once you've extracted the shared data. This prevents users from accidentally re-sharing stale data when they refresh the page.
  • Provide immediate visual feedback — show a toast, badge, or highlight to confirm that shared content was received successfully. Users need to know the share action worked.
  • Handle empty shares gracefully — not all share sources provide all fields. A share from a text selection might have no URL; a URL share might have no text. Your handler should work with whatever combination of fields arrives.
  • Use skipWaiting() and clients.claim() — these ensure your service worker takes control immediately after installation, which is critical for handling share target requests that may arrive right after the user installs your PWA.
  • Test with real device shares — the share flow behaves differently when invoked from different apps. Test shares from the browser, from a photos app, from a notes app, and from the clipboard to ensure consistent handling.
  • Consider using IndexedDB for large payloads — URL query parameters have size limits (~2000 characters in practice). For large text shares or metadata about many files, store the data in IndexedDB from the service worker and pass only an ID to the client page.
  • Add a robots.txt rule or meta tag — the /share-handler URL should not be indexed by search engines. It's an internal endpoint with no meaningful content for crawlers.

Browser Compatibility and Progressive Enhancement

Web Share Target is supported on Chromium-based browsers (Chrome, Edge, Opera) on Android, ChromeOS, and desktop platforms where PWA installation is available. As of 2025, Safari on iOS has begun implementing parts of the manifest-based share target but with some limitations — notably, only supporting URL shares and requiring the app to be added to the home screen through Safari's "Add to Home Screen" flow.

To implement progressive enhancement, feature-detect the relevant capabilities:

// Check if the browser supports Web Share Target (manifest-based)
// There's no direct API detection — instead check for PWA manifest support
// and service worker capability, which are prerequisites
const supportsShareTarget = 
  'serviceWorker' in navigator &&
  'BeforeInstallPromptEvent' in window;

// For sharing OUT from your app (Web Share API), check directly:
const supportsWebShare = 'share' in navigator;

if (supportsWebShare) {
  // Enable share buttons in your UI
  document.getElementById('share-button').classList.remove('hidden');
  
  // Optional: check for specific share capabilities
  const canShareFiles = 'canShare' in navigator && 
    navigator.canShare({ files: [new File([], 'test.txt', { type: 'text/plain' })] });
}

// For receiving shares (Web Share Target), the capability depends on
// PWA installation. Provide clear guidance to users about installing
// your PWA to enable share target functionality
if (supportsShareTarget) {
  // Show install prompt or educational UI about share target benefits
  showInstallEducation();
}

Even if share target isn't available, your app should still function normally — just without the ability to appear as a share destination. The share handling code in your service worker and client pages simply won't be invoked.

Common Pitfalls and Troubleshooting

Several issues tend to trip up developers implementing Web Share Target. Here are the most frequent ones and their solutions:

  • Manifest not linked correctly — ensure your HTML has <link rel="manifest" href="/manifest.json"> and the manifest is served with the correct MIME type (application/manifest+json). A wrong MIME type silently breaks manifest parsing.
  • Service worker scope mismatch — the action URL in your share_target must be within the service worker's scope. If your SW is registered at /app/ but your share handler is at /share-handler, the fetch event won't be intercepted.
  • Using the wrong form field names — the params object in your manifest maps share data keys to form field names. If you set "url": "link", you must retrieve formData.get('link') in your service worker, not formData.get('url').
  • Redirecting to a cross-origin URL — the redirect from your share handler must stay within your PWA's scope. Redirecting to an external domain will fail or cause the share to be lost.
  • Not handling the multipart boundary correctly — when using "multipart/form-data", ensure your service worker calls request.formData() exactly once. The form data stream can only be consumed once; subsequent calls will return empty data.
  • Missing HTTPS — service workers require a secure context (HTTPS or localhost). If your PWA is served over HTTP in production, the service worker won't register and share target won't work.

Conclusion

Web Share Target transforms your Progressive Web App from a passive web page into an active participant in the operating system's sharing ecosystem. By adding a few lines to your web app manifest and implementing a thoughtful service worker handler, you enable users to share content directly into your application — closing the gap between web and native experiences.

The implementation pattern is straightforward: declare your share target in the manifest, intercept the POST request in your service worker, extract the form data, and redirect to a client page that processes the shared content. With the complete code examples and best practices covered in this tutorial, you have everything needed to add share target capabilities to your own PWAs. The result is a more integrated, more useful application that meets users where they already are — right in the middle of their sharing workflow.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles