Web Share API: Native Share Dialog in JavaScript
AI generated
JS
() =>
JavaScript · Web Share API · PWA · Mobile UX · Progressive Enhancement
Web Share API: Native Share Dialog
Mastering navigator.share() and the Share Target API

Custom share buttons hardcoded to a handful of social media platforms are outdated. The Web Share API opens the operating system's native share dialog, and the user decides which app to use: WhatsApp, Telegram, Email, Notes, or anything else. The result is better UX with less code and no dependency on third-party SDKs.

10 min read Web Share API · Share Target · PWA · navigator.share() Android · iOS · Chrome · Safari · Edge

1. Why the Web Share API makes social buttons obsolete

The classic approach to share functionality on websites, a row of buttons for Twitter, Facebook, WhatsApp, LinkedIn and friends, has several fundamental problems. First, these buttons are tied to each platform's SDK, which loads tracking scripts and complicates privacy compliance. Second, the developer has to decide which platforms to offer, and almost always gets the selection wrong for some portion of users. Third, these buttons look out of place on mobile devices, because users are used to invoking the operating system's native share dialog on their smartphone.

The Web Share API solves all three problems at once. navigator.share() opens the system's own share dialog: the Material Design share sheet on Android, the UIActivityViewController on iOS. There, the user sees every app registered as a share target on their device: WhatsApp, Telegram, Gmail, Notes, AirDrop, any installed app. The developer writes zero SDK code and loads no external script. The result is a fully privacy-neutral share function that gives users more freedom while requiring less code.

navigator.share() accepts an options object with up to four properties: title (string), text (string), url (string) and files (array of File objects). The API returns a Promise that resolves once the user completes the share, and rejects with an AbortError if the user cancels the dialog, or with a NotAllowedError if the call does not happen inside a user gesture handler. Those are the two most common sources of error in a Web Share API integration.

The title, text and url properties are all optional, but at least one must be provided. In practice, url is the most consistent: almost every share app uses the URL. text is pre-filled as the message body by some apps (WhatsApp, for example), while others ignore it. title is used as the subject line (Email) or as the shared link's title in some implementations. The behavior is therefore app-specific and not fully controllable, which has the advantage that the target app itself knows best how to present the information.


// Complete Web Share API integration with all best practices

class ShareButton extends HTMLElement {
  #button
  #fallback

  connectedCallback() {
    this.innerHTML = `
      <button class="share-btn" type="button">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
          <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
          <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
        </svg>
        Share
      </button>
      <div class="share-fallback" hidden>
        <!-- Fallback: copy-to-clipboard button -->
        <button class="copy-btn" type="button">Copy link</button>
      </div>
    `
    this.#button = this.querySelector('.share-btn')
    this.#fallback = this.querySelector('.share-fallback')

    // Feature detection: show native share or fallback
    if ('share' in navigator) {
      this.#button.addEventListener('click', () => this.#nativeShare())
    } else {
      this.#button.hidden = true
      this.#fallback.hidden = false
      this.#fallback.querySelector('.copy-btn')
        .addEventListener('click', () => this.#copyFallback())
    }
  }

  async #nativeShare() {
    const shareData = {
      title: this.dataset.title || document.title,
      text: this.dataset.text || '',
      url: this.dataset.url || location.href,
    }

    try {
      await navigator.share(shareData)
      // Promise resolves when sharing is complete (or dismissed on some platforms)
      this.dispatchEvent(new CustomEvent('share-success', { bubbles: true }))
    } catch (err) {
      if (err.name === 'AbortError') {
        // User cancelled, not an error, do nothing
        return
      }
      // Unexpected error: fall through to clipboard fallback
      console.warn('navigator.share() failed:', err)
      await this.#copyFallback()
    }
  }

  async #copyFallback() {
    const url = this.dataset.url || location.href
    await navigator.clipboard.writeText(url)
    this.dispatchEvent(new CustomEvent('share-copied', { bubbles: true }))
  }
}

customElements.define('share-button', ShareButton)

3. Feature detection and Progressive Enhancement

The Web Share API is not available on every platform. The most reliable check is 'share' in navigator. Safari on iOS has supported the API since iOS 12.2, Chrome on Android since Chrome 61. Desktop support has improved considerably since 2023: Chrome and Edge on Windows and macOS support navigator.share(), while Firefox still does not, as of 2026, in its stable channel. Feature detection is therefore mandatory: no code should call navigator.share() without first checking whether the method exists.

Progressive Enhancement here means the following: the base functionality (share a link) always works. If the Web Share API is available, the native dialog is shown. If not, there is a fallback to "Copy link" using the Clipboard API. If the Clipboard API is also unavailable (which happens in insecure contexts without HTTPS), a plain text input remains, from which the user can copy manually. This three-tier degradation ensures the feature stays usable on every browser and in every network configuration.

4. Sharing files with navigator.share() and canShare()

File sharing via the Web Share API is Level 2 of the specification and requires an explicit check with navigator.canShare({ files }). There is good reason for this: not every share target supports file attachments, and not every browser implementation allows every file type. canShare() takes the same options object as share() and returns a boolean, synchronously, with no Promise involved. It is the only reliable way to check whether a specific share configuration is supported on the current device.

Allowed file types for the Web Share API are restricted by the specification to a "safe list" of types: images (image/png, image/jpeg, image/webp, image/gif), audio (audio/flac, audio/mp4, audio/mpeg, audio/ogg, audio/wav), video (video/mp4, video/ogg) and text (text/plain, text/csv, text/html, application/pdf). Executable files are deliberately excluded. A practical example: an image editing web app can share the edited image directly to WhatsApp or via AirDrop, with no server upload and no external dependencies.


// Sharing files: Level 2 Web Share API with canShare() guard

async function shareCanvasAsImage(canvas, filename = 'image.png') {
  // Convert canvas to Blob
  const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'))
  const file = new File([blob], filename, { type: 'image/png' })

  const shareData = {
    files: [file],
    title: 'My image',
    text: 'Created with mironsoft.de',
  }

  // canShare() is synchronous, check before calling share()
  if (!('share' in navigator) || !('canShare' in navigator)) {
    console.warn('Web Share API not available, fallback to download')
    return downloadFile(blob, filename)
  }

  if (!navigator.canShare(shareData)) {
    console.warn('File type not shareable on this device')
    return downloadFile(blob, filename)
  }

  try {
    await navigator.share(shareData)
  } catch (err) {
    if (err.name !== 'AbortError') {
      console.error('Share failed:', err)
      downloadFile(blob, filename)
    }
  }
}

function downloadFile(blob, filename) {
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = filename
  a.click()
  // Revoke object URL after download starts
  setTimeout(() => URL.revokeObjectURL(url), 1000)
}

5. User gesture: the hidden constraint

One of the most common sources of error when implementing the Web Share API is the user gesture requirement. navigator.share() may only be called as a direct response to a user action, that is, inside a click, touch or keyboard event handler. A call from a setTimeout, a Promise callback, or a fetch response handler is rejected with a NotAllowedError. Browsers enforce this to prevent share dialogs from being opened without the user's intent.

The problem typically shows up when the share call sits after an asynchronous operation: "load the data first, then share it." Because the original click event handler has long since finished by the time the fetch completes, the context is considered "untrusted." The solution: load or prepare the data before the user clicks "Share," and place the navigator.share() call directly in the synchronous part of the event handler. With the File API, an exception is possible: Blobs can be created ahead of time, as long as the final share() call happens synchronously inside the event handler.

6. Share Target API: registering a PWA as a receiver

The Share Target API is the reverse of the Web Share API: instead of sharing content, the PWA registers itself as a receiver for content from other apps. When a user opens an image in the gallery app and taps "Share," the installed PWA appears in the share dialog alongside WhatsApp and other apps. Registration happens in the web app manifest via the share_target key.

The Share Target API supports two modes: GET parameters for simple text/URL shares, and POST multipart for file shares. In GET mode, the operating system opens the PWA with a URL like /share?title=...&text=...&url=.... In POST mode, it sends a multipart form request to a service worker. The service worker receives the data, stores it in IndexedDB or the cache, and then opens the correct window with the data. The implementation requires both an active service worker and a correct manifest configuration.


// web app manifest snippet: Share Target API registration
// Add to manifest.json:
// {
//   "share_target": {
//     "action": "/share-handler",
//     "method": "POST",
//     "enctype": "multipart/form-data",
//     "params": {
//       "title": "title",
//       "text": "text",
//       "url": "url",
//       "files": [{ "name": "media", "accept": ["image/*", "video/*"] }]
//     }
//   }
// }

// Service worker: handle incoming share via fetch event
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url)

  if (url.pathname === '/share-handler' && event.request.method === 'POST') {
    event.respondWith(handleShareTarget(event.request))
  }
})

async function handleShareTarget(request) {
  const formData = await request.formData()

  const title = formData.get('title') || ''
  const text  = formData.get('text')  || ''
  const url   = formData.get('url')   || ''
  const files = formData.getAll('media') // File objects

  // Store shared data for the app window to pick up
  const cache = await caches.open('share-data')
  const payload = JSON.stringify({ title, text, url, fileCount: files.length })
  await cache.put('/share-payload', new Response(payload))

  // Store file blobs
  for (let i = 0; i < files.length; i++) {
    const blob = files[i]
    await cache.put(`/share-file-${i}`, new Response(blob))
  }

  // Open or focus the share page
  const clients = await self.clients.matchAll({ type: 'window' })
  if (clients.length > 0) {
    clients[0].focus()
    clients[0].navigate('/app?share=1')
  } else {
    await self.clients.openWindow('/app?share=1')
  }

  // Respond with redirect, browser follows it after sharing
  return Response.redirect('/app?share=1', 303)
}

7. Fallback strategies for desktop and Firefox

For desktop browsers without Web Share API support, Firefox in particular, which still does not support the API in its stable channel as of 2026, an ergonomic fallback is needed. The best fallback strategy in 2026 is the combination of the Clipboard API and a small, modal-style "share panel." The panel shows a pre-filled link input with a "Copy" button and, optionally, direct links to the most important platforms. This panel only appears when 'share' in navigator evaluates to false.

The Clipboard API (navigator.clipboard.writeText()) is also a Promise-based API and requires a secure context (HTTPS or localhost). On older browsers or without HTTPS, a fallback to document.execCommand('copy') is needed, which is considered deprecated but is still widely supported. The three-layer strategy (native share, then Clipboard, then execCommand) covers nearly every browser scenario. The fallback should look good and be useful, not be treated as a "workaround" with an unreadable raw link.

8. Tracking share events without violating privacy

An often-overlooked benefit of the Web Share API: it does not tell the website which app the user chose, or whether the share was actually sent. The Promise returned by navigator.share() resolves as soon as the dialog is opened and a selection is made, not when the message is actually delivered in WhatsApp. That is excellent from a privacy standpoint, but for analytics purposes it means working within what is actually measurable: the share dialog was opened (resolved) versus cancelled (AbortError).

For a privacy-compliant measurement of share events, it is enough to log an event on a successful await navigator.share(). A custom event (share-intent) can be triggered with dispatchEvent and picked up by an analytics system. Important: do not store platform-specific information, because the Web Share API deliberately does not provide it. That is the correct approach: analytics should know that a share happened, not how or where, which is privacy by design.

Browser/Platform navigator.share() File share Share Target
Chrome Android Yes (since 61) Yes Yes
Safari iOS Yes (since 12.2) Yes (since 15) No
Chrome Desktop Yes (since 89) Yes Partial
Safari macOS Yes (since 12.1) Limited No
Firefox No (2026) No No
Edge Desktop Yes Yes Partial

9. Web Share API browser comparison

Browser support for the Web Share API is excellent on mobile platforms in 2026: nearly every smartphone, iOS and Android alike, can open the native share dialog with a single navigator.share() call. On desktop platforms the picture is more nuanced. Chrome and Edge on macOS and Windows support the API and open an OS-native share sheet or a simple email/clipboard interface. Safari on macOS opens the AirDrop/macOS sharing dialog. Firefox is missing across every platform.

The fact that Firefox does not support the Web Share API is not a critical problem for most web apps, since Firefox users, especially on desktop, tend to copy the URL manually or use the browser's own sharing features anyway. The important thing is that feature detection and the fallback are implemented cleanly. A website that shows no share button to Firefox users is better than one that responds to a click with a JavaScript error. Progressive Enhancement is not an optional best practice here, it is a functional necessity.

10. Summary

The Web Share API is one of the most elegant modern browser APIs: little code, a large UX improvement, zero external dependencies and built-in privacy. navigator.share() for text/URL shares and navigator.canShare() plus file sharing for files cover most use cases. The Share Target API turns a PWA into a full member of the sharing ecosystem: the app appears in the native share dialog of other applications.

The implementation checklist for the Web Share API: feature detection with 'share' in navigator, strictly respect user gesture contexts, distinguish AbortError from real errors, call canShare() before file shares, and implement a clean fallback for Firefox and desktop browsers. Anyone who covers these points replaces a collection of third-party share buttons with a single, clean API integration that delivers a noticeably better user experience on mobile devices.

Web Share API, the essentials at a glance

Feature Detection

Check 'share' in navigator before every call. Firefox does not support the API, implement a fallback to the Clipboard API.

User Gesture Required

navigator.share() may only be called directly inside a click/touch handler. Asynchronous chains destroy the gesture context.

canShare() for files

Check canShare({ files }) synchronously before every file share. Only allowed file types (images, audio, video, text) are accepted.

Share Target API

Register share_target in the manifest, set up a service worker for the POST handler, the PWA then appears in the native share dialog of other apps.

Mironsoft

Progressive Web Apps, browser APIs and mobile UX optimization

PWA with Web Share API and Share Target?

We implement the Web Share API with correct feature detection, a complete fallback and Share Target integration in your PWA, clean, privacy-compliant and without external sharing libraries.

Share integration

navigator.share() with a correct fallback and file share support

Share Target API

Manifest configuration and service worker for received shares

PWA audit

Complete PWA analysis: manifest, service worker, installability and web APIs

11. FAQ: Web Share API

1What happens without a feature check on unsupported browsers?
TypeError: navigator.share is not a function. Always check 'share' in navigator, never call it directly without a check.
2Can I find out which app was chosen?
No. The API deliberately does not expose this info. The Promise only resolves that a selection was made, not which app or whether it was actually sent.
3Why NotAllowedError?
No user gesture context. navigator.share() must sit directly inside a click/touch handler, not after an await or setTimeout.
4Which file types are allowed?
Images, audio, video, text and PDF. Not executable files. Always check canShare({ files }) before calling share().
5Web Share API vs. Share Target API?
navigator.share() sends content to other apps. Share Target receives content from other apps. Together they form a complete PWA sharing ecosystem.
6Does the page need HTTPS?
Yes. A secure context (HTTPS or localhost) is mandatory. On HTTP, navigator.share is not available.
7Implementing Share Target for receiving files?
Manifest: share_target with method POST, multipart. Service worker: intercept the fetch event, read the FormData, store files in the cache, then open/focus the window.
8Distinguishing AbortError from real errors?
catch: if (err.name === 'AbortError') return; the user cancelled, no error. All other names: show a fallback to the clipboard.
9Product sharing in an e-commerce shop?
Yes, an excellent use case. title: product name, text: description, url: product URL. Native dialog on mobile, clipboard fallback for desktop.
10Loading data dynamically before the share?
Preload the data on page load or on hover. Then call navigator.share() synchronously inside the click handler, not after an await inside the handler itself.