Build Web Share API with Claude Code (2026)

Claude Code for Web Share API Workflow Tutorial

The Web Share API is a powerful browser feature that enables web applications to invoke the native sharing capabilities of a user’s device. When combined with Claude Code’s development workflow, you can create smooth sharing experiences that feel natural and efficient. This tutorial walks you through building a solid Web Share API workflow using Claude Code as your development assistant.

Understanding the Web Share API

The Web Share API provides a navigator.share() method that opens the native share dialog on the user’s device. This API is particularly valuable because it allows web apps to access the same sharing capabilities that native apps use, eliminating the need for custom share buttons for each platform.

Before diving into the workflow, it’s essential to understand what the Web Share API offers:

  • Native share dialogs - Users see the same sharing options they’d get from a native app
  • No authentication required - Uses the user’s existing logged-in accounts on the device
  • Cross-platform support - Works on mobile browsers (Android Chrome, Safari on iOS) and desktop Chrome/Edge

However, the API has limitations you should be aware of. It requires a secure context (HTTPS), only works in user-initiated contexts, and falls back gracefully when unavailable.

Browser Support at a Glance

Understanding where the Web Share API works. and where it does not. is essential before committing to it as your primary sharing strategy.

Browser / Platform navigator.share() Files support Web Share Target
Chrome (Android) Yes (since v61) Yes (since v75) Yes
Safari (iOS 12.4+) Yes Partial No
Safari (macOS 12.1+) Yes Partial No
Chrome (Desktop, Win/Mac) Yes (since v89) Yes No
Edge (Chromium) Yes (since v79) Yes No
Firefox (all) No No No
Samsung Internet Yes Yes Yes

Firefox’s lack of support makes fallback handling non-optional. As of early 2026, roughly 30–35% of desktop browser sessions do not support navigator.share(), so you must always have an alternate path ready.

What Can You Share?

The share payload accepts up to four fields:

{
 title: "String. the share title",
 text: "String. body text for messaging apps",
 url: "String. a URL to attach",
 files: [/* array of File objects */]
}

At least one field must be present, but not all targets use all fields. Twitter typically uses url. iMessage uses text and url. WhatsApp uses text with the URL appended inline. Designing for this fragmentation means keeping your text field self-contained and including the URL both in url and appended to text when broad compatibility matters.

Setting Up Your Claude Code Workflow

Claude Code can significantly accelerate your Web Share API implementation. Here’s how to structure your development workflow:

Step 1: Define Feature Requirements

Before writing code, use Claude Code to outline your sharing requirements. Describe your use case and ask for a feature specification. For example:

“Help me design a share feature for an article bookmarking app. Users should be able to share article titles, URLs, and optional notes to any app on their device.”

Claude Code will help you identify edge cases and suggest implementation approaches.

A well-formed requirements prompt to Claude Code should specify:

  • Target content types. URLs only, or URLs plus images, files, or generated summaries
  • Fallback priority. clipboard copy, custom share modal, or social buttons
  • Analytics requirements. do you need to know when shares happen and which content is shared most
  • Framework context. vanilla JS, React, Vue, Svelte, etc.

Starting with a clear requirements conversation rather than jumping straight to code produces implementations with fewer design-level rework cycles.

Step 2: Feature Detection and Graceful Degradation

Always check for API availability before attempting to use it. Claude Code can help you write solid feature detection:

function isShareAPIAvailable() {
 return navigator.share !== undefined;
}
async function shareContent(data) {
 if (!isShareAPIAvailable()) {
 // Fallback: copy to clipboard or show custom share modal
 return fallbackShare(data);
 }
 try {
 await navigator.share({
 title: data.title,
 text: data.description,
 url: data.url
 });
 return { success: true };
 } catch (error) {
 if (error.name === 'AbortError') {
 // User cancelled - handle gracefully
 return { success: false, cancelled: true };
 }
 throw error;
 }
}

Notice the AbortError handling, this occurs when users cancel the share dialog, and your code should handle this gracefully without showing error messages.

For file sharing, add a separate capability check. The navigator.canShare() method lets you test whether a specific payload. including files. is shareable before calling navigator.share():

function canShareFiles(files) {
 if (!navigator.canShare) return false;
 return navigator.canShare({ files });
}
async function shareWithFiles(data, files) {
 if (canShareFiles(files)) {
 return navigator.share({ ...data, files });
 }
 // Fall back to sharing without files
 return navigator.share(data);
}

This two-tier check prevents runtime errors when a browser supports navigator.share() but rejects a specific file type or size in the payload.

Step 3: Building a Reusable Share Component

Claude Code can help you create a reusable component that handles all the complexity:

class ShareManager {
 constructor(options = {}) {
 this.fallbackHandler = options.fallback || this.defaultFallback;
 this.onShareSuccess = options.onSuccess || (() => {});
 this.onShareError = options.onError || console.error;
 }
 async share(shareData) {
 if (isShareAPIAvailable()) {
 return this.nativeShare(shareData);
 }
 return this.fallbackHandler(shareData);
 }
 async nativeShare(data) {
 try {
 await navigator.share({
 title: data.title,
 text: data.text,
 url: data.url,
 files: data.files // Available in some browsers
 });
 this.onShareSuccess();
 } catch (error) {
 if (error.name !== 'AbortError') {
 this.onShareError(error);
 }
 }
 }
 defaultFallback(data) {
 // Copy URL to clipboard as default fallback
 navigator.clipboard.writeText(data.url);
 alert('Link copied to clipboard!');
 }
}

This component provides a clean API that handles both native sharing and graceful fallbacks.

A more production-ready version upgrades the defaultFallback with a proper clipboard API call and user feedback pattern:

async defaultFallback(data) {
 try {
 await navigator.clipboard.writeText(data.url || data.text || '');
 this.onShareSuccess({ method: 'clipboard' });
 } catch (clipboardError) {
 // Clipboard API also requires secure context and user gesture
 // Last resort: prompt with a text field for manual copy
 this.showManualCopyDialog(data);
 }
}
showManualCopyDialog(data) {
 const input = document.createElement('input');
 input.value = data.url || '';
 input.setAttribute('readonly', '');
 input.style.position = 'absolute';
 input.style.left = '-9999px';
 document.body.appendChild(input);
 input.select();
 document.execCommand('copy');
 document.body.removeChild(input);
}

The three-tier fallback chain. native share, clipboard API, manual copy. covers virtually every browser scenario your users will encounter.

Advanced Patterns for Production Apps

Dynamic Content Preparation

For optimal share experiences, prepare content dynamically based on the current context. Claude Code can help you implement smart previews:

async function prepareShareData(articleId) {
 const article = await fetchArticle(articleId);
 return {
 title: article.title,
 text: `Check out this article: ${article.title}`,
 url: `${window.location.origin}/articles/${articleId}`,
 // Generate preview image for social platforms
 files: article.hasImage ? [await fetchPreviewImage(article.imageUrl)] : []
 };
}

Dynamic content preparation also means thinking about character limits. Different share targets impose different constraints:

Platform Title limit Text limit Notes
Twitter/X Ignored ~280 chars (URL counts) URL auto-appended
WhatsApp Ignored ~65,000 chars URL appended to text
iMessage Used as subject Full text Both shown
Email Used as subject Full body All fields used
Slack Ignored Used as message URL unfurled

Because you cannot predict which app the user will choose, the safest strategy is to keep text under 280 characters while including the URL explicitly in the url field. This maximizes compatibility without truncation on any target.

Tracking Share Events with Analytics

Most analytics platforms do not automatically capture Web Share API events. Add tracking around your share calls to understand what content drives sharing behavior:

class ShareManager {
 constructor(options = {}) {
 this.analytics = options.analytics || null;
 // ...other options
 }
 async nativeShare(data) {
 try {
 await navigator.share({
 title: data.title,
 text: data.text,
 url: data.url
 });
 this.trackShareEvent('native_share', 'success', data);
 this.onShareSuccess();
 } catch (error) {
 if (error.name === 'AbortError') {
 this.trackShareEvent('native_share', 'cancelled', data);
 } else {
 this.trackShareEvent('native_share', 'error', data);
 this.onShareError(error);
 }
 }
 }
 trackShareEvent(method, outcome, data) {
 if (!this.analytics) return;
 this.analytics.track('share_attempt', {
 method,
 outcome,
 content_url: data.url,
 content_title: data.title
 });
 }
}

Tracking cancelled separately from error is important. A high cancellation rate on a particular piece of content may indicate users are opening the share dialog by accident, while a high success rate shows genuine sharing intent. Both signals are actionable.

Integration with State Management

In modern frameworks, integrate the ShareManager with your state management system:

// React example with context
const ShareContext = createContext();
function ShareProvider({ children }) {
 const shareManager = useMemo(() => new ShareManager({
 onSuccess: () => toast.success('Shared successfully!'),
 onError: (err) => toast.error('Failed to share')
 }), []);
 return (
 <ShareContext.Provider value={shareManager}>
 {children}
 </ShareContext.Provider>
 );
}

A custom hook simplifies the consumer interface:

export function useShare() {
 const shareManager = useContext(ShareContext);
 const [shareState, setShareState] = useState({ status: 'idle' });
 const share = useCallback(async (data) => {
 setShareState({ status: 'pending' });
 try {
 await shareManager.share(data);
 setShareState({ status: 'success' });
 } catch (err) {
 setShareState({ status: 'error', error: err });
 } finally {
 setTimeout(() => setShareState({ status: 'idle' }), 3000);
 }
 }, [shareManager]);
 return { share, shareState };
}
// Usage in any component
function ArticleCard({ article }) {
 const { share, shareState } = useShare();
 return (
 <button
 onClick={() => share({ title: article.title, url: article.url })}
 disabled={shareState.status === 'pending'}
 >
 {shareState.status === 'success' ? 'Shared!' : 'Share'}
 </button>
 );
}

This pattern keeps share state co-located with the component that needs it while delegating all sharing logic to the context-provided manager.

Handling Web Share Target

For receiving shared content, implement the Web Share Target API in your PWA manifest:

{
 "share_target": {
 "action": "/share-handler",
 "method": "GET",
 "params": {
 "title": "title",
 "text": "text",
 "url": "url"
 }
 }
}

Claude Code can help you set up the server-side handler for these incoming shares.

For POST-based sharing that includes files, the manifest entry is more involved:

{
 "share_target": {
 "action": "/share-handler",
 "method": "POST",
 "enctype": "multipart/form-data",
 "params": {
 "title": "title",
 "text": "text",
 "url": "url",
 "files": [
 {
 "name": "media",
 "accept": ["image/jpeg", "image/png", "image/gif", "image/webp"]
 }
 ]
 }
 }
}

The service worker handles the incoming POST before the page loads:

// service-worker.js
self.addEventListener('fetch', (event) => {
 const url = new URL(event.request.url);
 if (event.request.method === 'POST' && url.pathname === '/share-handler') {
 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');
 // Store the incoming share data for the page to consume
 const cache = await caches.open('share-target-data');
 await cache.put('/pending-share', new Response(JSON.stringify({
 title, text, url, fileCount: files.length
 })));
 // Redirect to the app with a signal
 return Response.redirect('/app?incoming-share=1', 303);
}

This service worker intercept pattern is what makes Web Share Target work. the incoming POST from the OS sharing system gets intercepted before the page loads, data is cached, and then the app page reads from cache on startup.

Comparing Share Strategies

When deciding between the Web Share API and traditional share buttons, the trade-offs are practical and measurable:

Factor Web Share API Custom share buttons
User experience Native OS dialog Custom UI in-page
Browser support ~70% (desktop+mobile) 100%
Maintenance No target-specific code Per-platform SDK/link
Analytics Manual instrumentation Platform SDKs help
File sharing Yes (where supported) Rarely possible
Privacy No third-party scripts Third-party scripts load
New platforms Automatic (OS-level) Requires code update

For content-focused apps where sharing is a primary action. news readers, bookmarking tools, recipe apps. the Web Share API with a solid fallback is almost always the better choice. For marketing pages where tracking platform-specific click-through rates matters, custom buttons is worth the extra complexity.

Testing Your Implementation

Testing share functionality requires more care than most browser APIs because navigator.share() requires a user gesture and a secure context.

Unit Testing the Logic

Isolate your share logic from the browser API to make it unit-testable:

// shareUtils.js. pure logic, no browser API calls
export function buildSharePayload(article, baseUrl) {
 return {
 title: article.title,
 text: article.summary || `Read: ${article.title}`,
 url: `${baseUrl}/articles/${article.slug}`
 };
}
export function isValidSharePayload(payload) {
 return !!(payload.title || payload.text || payload.url);
}
// shareUtils.test.js
import { buildSharePayload, isValidSharePayload } from './shareUtils';
test('builds correct payload from article', () => {
 const article = { title: 'Test', slug: 'test', summary: 'A summary' };
 const payload = buildSharePayload(article, 'https://example.com');
 expect(payload.url).toBe('https://example.com/articles/test');
 expect(payload.text).toBe('A summary');
});
test('rejects empty payloads', () => {
 expect(isValidSharePayload({})).toBe(false);
 expect(isValidSharePayload({ title: 'Hello' })).toBe(true);
});

Ask Claude Code to generate these unit tests for your specific payload-building logic. The separation of pure utility functions from browser API calls makes the pure functions trivially testable.

End-to-End Testing with Playwright

Playwright can mock navigator.share() to test the full UI flow:

// share.spec.js
test('share button triggers share dialog', async ({ page }) => {
 // Mock the Web Share API
 await page.addInitScript(() => {
 window.navigator.share = async (data) => {
 window.__lastShareData = data;
 };
 });
 await page.goto('/articles/my-article');
 await page.click('[data-testid="share-button"]');
 const shareData = await page.evaluate(() => window.__lastShareData);
 expect(shareData.url).toContain('/articles/my-article');
 expect(shareData.title).toBeTruthy();
});
test('falls back to clipboard when share API unavailable', async ({ page }) => {
 await page.addInitScript(() => {
 delete window.navigator.share;
 window.navigator.clipboard = {
 writeText: async (text) => { window.__clipboardText = text; }
 };
 });
 await page.goto('/articles/my-article');
 await page.click('[data-testid="share-button"]');
 const clipboardText = await page.evaluate(() => window.__clipboardText);
 expect(clipboardText).toContain('/articles/my-article');
});

Use Claude Code to scaffold these Playwright tests. Describe your share button’s selector and the expected outcomes; Claude generates the mock setup and assertions.

Best Practices and Actionable Advice

  1. Always provide fallbacks - Not all browsers support the Web Share API. Implement clipboard copying or custom share modals as alternatives.

  2. Test on real devices - Emulators don’t always accurately represent share dialog behavior. Test on actual mobile devices.

  3. Optimize shared content - Craft compelling titles and descriptions since shared content often appears in social feeds and messaging apps.

  4. Handle the AbortError - Don’t treat user cancellation as an error; it’s normal behavior.

  5. Consider file sharing - The Files API extension allows sharing images and documents directly from your web app.

  6. Track share analytics - Implement event tracking to understand what content users share most.

  7. Keep text under 280 characters - Since you cannot know which app the user will choose, staying under Twitter’s character limit ensures the full text appears in every target without truncation.

  8. Gate the share button on API availability. Rather than showing a share button that falls back invisibly, consider showing the native share button only when navigator.share is available and a dedicated “Copy link” button otherwise. This reduces user confusion about what the button does.

  9. Debounce the share handler. Rapid taps on a mobile share button can trigger multiple share dialogs in sequence. Debounce or disable the button between the call and its resolution to prevent this.

function createDebouncedShare(shareManager) {
 let pending = false;
 return async function debouncedShare(data) {
 if (pending) return;
 pending = true;
 try {
 await shareManager.share(data);
 } finally {
 pending = false;
 }
 };
}
  1. Verify HTTPS in development. The Web Share API silently does nothing on HTTP. If your local dev environment uses HTTP, switch to HTTPS (many frameworks support --https flags) or test directly on a deployed staging environment to avoid mysterious non-behavior during development.

Prompt Patterns That Work Well with Claude Code

Claude Code accelerates Web Share API work most when you give it specific, constrained prompts rather than open-ended ones. A few patterns that produce high-quality output:

Prompt for a feature with explicit constraints:

“Write a useShare React hook that wraps navigator.share with a clipboard fallback. The hook should return { share, isPending, lastOutcome }. Do not use any external libraries.”

Prompt for a specific error scenario:

“My navigator.share call sometimes throws a NotAllowedError on iOS Safari. Write the error handling code that catches this, logs it, and falls back to clipboard. Explain when iOS Safari throws this error.”

Prompt for accessibility:

“Review this share button component and identify any accessibility issues. The button triggers navigator.share on click.”

Prompt for a test:

“Write a Vitest unit test for this buildSharePayload function. Cover: normal article, article with no summary, article with a very long title (over 300 chars), and article with a null slug.”

Each of these prompts gives Claude Code enough context to produce directly usable code without multiple clarification rounds.

Conclusion

The Web Share API, combined with Claude Code’s development workflow, enables you to create smooth sharing experiences that rival native applications. By following this tutorial’s patterns, feature detection, graceful degradation, and reusable components, you’ll build solid sharing functionality that works across all browsers and devices.

The core implementation is straightforward, but production quality requires attention to fallback depth, analytics instrumentation, file sharing capability detection, and thorough testing across both native-share and fallback paths. Claude Code handles the repetitive scaffolding and edge-case analysis, letting you focus on the design decisions that actually differentiate your sharing experience.

Remember to always provide fallback options for unsupported browsers, and use Claude Code to iterate quickly on your implementation. With these tools and practices, you can create share features that feel natural and effective for your users.


Try it: Paste your error into our Error Diagnostic for an instant fix.

I'm a solo developer in Vietnam. 50K Chrome extension users. $500K+ on Upwork. 5 Claude Max subscriptions running agent fleets in parallel. These are my actual CLAUDE.md templates, orchestration configs, and prompts. Not a course. Not theory. The files I copy into every project before I write a line of code. **[See what's inside →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-default&utm_campaign=claude-code-for-web-share-api-workflow-tutorial)** $99 once. Free forever. 47/500 founding spots left.

Related Reading

Built by theluckystrike. More at zovo.one

Find the right skill → Browse 155+ skills in our Skill Finder.