Front-end Engineering Lab
PatternsMobile & PWA

Web Share API

Use native share menu to let users share content via installed apps on their device.

The Problem

Without Web Share API:

  • Custom share buttons: One for each platform
  • Poor UX: Must copy link manually
  • Limited options: Can't share to all apps
  • Not native: Doesn't feel integrated

Solution

Use Web Share API to open native share menu.

/**
 * Share data interface
 */
interface ShareData {
  title?: string;
  text?: string;
  url?: string;
  files?: File[];
}

/**
 * Web Share manager
 */
class WebShareManager {
  /**
   * Check if Web Share is supported
   */
  public static isSupported(): boolean {
    return 'share' in navigator;
  }

  /**
   * Check if can share files
   */
  public static canShareFiles(): boolean {
    return 'canShare' in navigator && typeof navigator.canShare === 'function';
  }

  /**
   * Share content
   */
  public static async share(data: ShareData): Promise<boolean> {
    if (!this.isSupported()) {
      console.warn('Web Share API not supported');
      return false;
    }

    // Check if can share this data
    if (data.files && this.canShareFiles()) {
      if (!navigator.canShare(data)) {
        console.warn('Cannot share these files');
        return false;
      }
    }

    try {
      await navigator.share(data);
      console.log('Shared successfully');
      return true;
    } catch (error) {
      if ((error as Error).name === 'AbortError') {
        console.log('Share cancelled by user');
      } else {
        console.error('Share failed:', error);
      }
      return false;
    }
  }

  /**
   * Share current page
   */
  public static async sharePage(): Promise<boolean> {
    return this.share({
      title: document.title,
      text: document.querySelector('meta[name="description"]')?.getAttribute('content') || '',
      url: window.location.href,
    });
  }

  /**
   * Share text
   */
  public static async shareText(text: string, title?: string): Promise<boolean> {
    return this.share({ text, title });
  }

  /**
   * Share URL
   */
  public static async shareURL(url: string, title?: string, text?: string): Promise<boolean> {
    return this.share({ url, title, text });
  }

  /**
   * Share image
   */
  public static async shareImage(file: File, title?: string, text?: string): Promise<boolean> {
    if (!this.canShareFiles()) {
      console.warn('File sharing not supported');
      return false;
    }

    return this.share({
      files: [file],
      title,
      text,
    });
  }

  /**
   * Share multiple files
   */
  public static async shareFiles(files: File[], title?: string, text?: string): Promise<boolean> {
    if (!this.canShareFiles()) {
      console.warn('File sharing not supported');
      return false;
    }

    return this.share({
      files,
      title,
      text,
    });
  }

  /**
   * Fallback to clipboard
   */
  public static async copyToClipboard(text: string): Promise<boolean> {
    try {
      await navigator.clipboard.writeText(text);
      console.log('Copied to clipboard');
      return true;
    } catch {
      // Fallback for older browsers
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.style.position = 'fixed';
      textarea.style.opacity = '0';
      document.body.appendChild(textarea);
      textarea.select();
      
      const success = document.execCommand('copy');
      document.body.removeChild(textarea);
      
      return success;
    }
  }

  /**
   * Share with fallback
   */
  public static async shareOrCopy(data: ShareData): Promise<void> {
    if (this.isSupported()) {
      await this.share(data);
    } else {
      // Fallback to copy link
      const url = data.url || window.location.href;
      const success = await this.copyToClipboard(url);
      
      if (success) {
        alert('Link copied to clipboard!');
      }
    }
  }
}

/**
 * Share button component
 */
class ShareButton {
  private button: HTMLButtonElement;
  private shareData: ShareData;

  constructor(buttonId: string, shareData: ShareData) {
    this.button = document.getElementById(buttonId) as HTMLButtonElement;
    this.shareData = shareData;
    this.setupButton();
  }

  private setupButton(): void {
    // Hide if not supported
    if (!WebShareManager.isSupported()) {
      this.button.style.display = 'none';
      return;
    }

    this.button.addEventListener('click', async () => {
      await WebShareManager.share(this.shareData);
    });
  }
}

/**
 * Screenshot share
 */
class ScreenshotShare {
  /**
   * Capture and share screenshot
   */
  public static async captureAndShare(): Promise<void> {
    if (!('html2canvas' in window)) {
      console.error('html2canvas not loaded');
      return;
    }

    // Capture screenshot (requires html2canvas library)
    const canvas = await (window as Window & { html2canvas: (el: HTMLElement) => Promise<HTMLCanvasElement> }).html2canvas(document.body);
    
    // Convert to blob
    const blob = await new Promise<Blob>((resolve) => {
      canvas.toBlob((blob) => resolve(blob!), 'image/png');
    });

    // Create file
    const file = new File([blob], 'screenshot.png', { type: 'image/png' });

    // Share
    await WebShareManager.shareImage(file, 'Check this out!');
  }
}

React Component

import { useState } from 'react';

function ShareButton({ title, text, url }: ShareData) {
  const [isSupported] = useState(() => 'share' in navigator);

  const handleShare = async () => {
    try {
      await navigator.share({ title, text, url });
    } catch (error) {
      if ((error as Error).name !== 'AbortError') {
        console.error('Share failed:', error);
      }
    }
  };

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(url || window.location.href);
      alert('Link copied!');
    } catch (error) {
      console.error('Copy failed:', error);
    }
  };

  if (!isSupported) {
    return (
      <button onClick={handleCopy}>
        Copy Link
      </button>
    );
  }

  return (
    <button onClick={handleShare}>
      Share
    </button>
  );
}

// Usage
function ArticlePage() {
  return (
    <div>
      <h1>Article Title</h1>
      <ShareButton
        title="Article Title"
        text="Check out this article!"
        url={window.location.href}
      />
    </div>
  );
}

Usage Examples

// Share current page
await WebShareManager.sharePage();

// Share custom URL
await WebShareManager.shareURL(
  'https://example.com',
  'Check this out!',
  'This is an amazing website'
);

// Share text
await WebShareManager.shareText('Hello World!', 'My message');

// Share image
const imageFile = new File([blob], 'photo.jpg', { type: 'image/jpeg' });
await WebShareManager.shareImage(imageFile, 'My Photo');

// Share with fallback
await WebShareManager.shareOrCopy({
  title: 'Amazing Article',
  url: 'https://example.com/article',
});

HTML Integration

<button onclick="shareContent()">Share</button>

<script>
async function shareContent() {
  if (navigator.share) {
    try {
      await navigator.share({
        title: 'My Page',
        text: 'Check this out!',
        url: window.location.href,
      });
    } catch (err) {
      console.log('Share failed', err);
    }
  } else {
    alert('Web Share not supported');
  }
}
</script>

Best Practices

  1. Check support: Feature detection
  2. User initiated: Must be click/tap
  3. Handle cancellation: User can close share menu
  4. Provide fallback: Copy to clipboard
  5. Track shares: Analytics for engagement
  6. Short text: Keep concise
  7. Include URL: Always provide share link

Web Share API increases sharing by 200% compared to custom share buttons and provides native feel.

On this page