Native Device Integration Patterns

Modern web applications bridge browser sandboxes and native hardware through four primary APIs: the Web Share API, Clipboard API, File System Access API, and Web NFC. Every pattern in this section is grounded in three non-negotiable mandates: secure origin enforcement, explicit feature detection before any API call, and a fallback chain that keeps core functionality intact when hardware hooks are absent or denied.

Four principles apply across every hardware API covered here:

  • Secure origins are mandatory — every API requires window.isSecureContext to be true; serve over https:// or localhost
  • Feature detection precedes invocation — check for the existence of navigator.share, NDEFReader, showOpenFilePicker, or navigator.clipboard before calling them
  • Payload validation prevents silent failures — MIME type allowlists differ across iOS, Android, and desktop; use navigator.canShare() to validate before constructing a share payload
  • Progressive enhancement is the architecture — native hooks augment baseline workflows; they never replace them

API Surface Reference

The table below maps each API to its primary entry point, the minimum gesture requirement, and the DOMException names thrown on failure. Keep this as a quick reference when writing error boundaries.

API Primary interface User gesture required Common thrown exceptions
Web Share navigator.share(data) Yes — click or touchstart AbortError, NotAllowedError, DataError
Clipboard write navigator.clipboard.writeText() / write() Yes (Chrome; Safari is more permissive) NotAllowedError
File System Access showOpenFilePicker() / showSaveFilePicker() Yes AbortError, NotAllowedError
Web NFC new NDEFReader().scan() / write() Yes NotAllowedError, NotSupportedError

Secure Context Enforcement

Hardware APIs expose sensitive device capabilities. Browsers enforce strict origin policies to prevent unauthorised access: every API in this section requires the page to be served over https:// or localhost. Understanding secure context requirements covers the full browser enforcement model, including how mixed-content warnings interact with window.isSecureContext.

Validate window.isSecureContext at module initialisation — not at call time — so that restricted UI states are established before any render cycle completes. Never assume a secure context in staging environments that proxy traffic without TLS termination.

User-gesture requirements compound the secure-context gate. Even in a secure context, navigator.share and showOpenFilePicker will throw NotAllowedError if called outside a short-lived user activation window. Bind API triggers to explicit click or touchstart handlers, not to lifecycle hooks, scroll events, or setTimeout callbacks.

/**
 * Validates secure context and user activation before any hardware API call.
 * Call this as the first statement inside every event handler that touches native APIs.
 */
export function assertHardwarePrerequisites() {
  if (!window.isSecureContext) {
    throw new DOMException(
      'Hardware APIs require a secure context (HTTPS or localhost).',
      'SecurityError'
    );
  }
  // navigator.userActivation is available in Chrome 72+, Edge 79+, Firefox 120+
  if (navigator.userActivation && !navigator.userActivation.isActive) {
    throw new DOMException(
      'Hardware API called outside a user activation window.',
      'NotAllowedError'
    );
  }
}

Feature Detection and Capability Mapping

Runtime crashes from missing navigator properties break telemetry pipelines and create silent UX regressions. Build a capability matrix once at application initialisation and propagate it through your state management layer — avoid repeated synchronous checks on every component mount.

For the Web Share API, navigator.canShare(payload) does double duty: it confirms API availability and validates the specific payload structure against the browser’s MIME type allowlist. Different platforms enforce different allowlists — iOS Safari accepts files only for certain MIME types, while Chrome on Android is more permissive. Implementing navigator.canShare() for graceful fallbacks provides a full breakdown of per-platform allowlists and validation strategies.

/**
 * Builds a capability map for native hardware APIs.
 * Resolve this once at app init; store the result in application state.
 */
export async function buildCapabilityMatrix() {
  if (!window.isSecureContext) {
    return { webShare: false, nfc: false, fileSystemAccess: false, clipboard: false };
  }

  // canShare with a test payload validates both API availability and payload schema support
  const webShare = typeof navigator.canShare === 'function'
    ? navigator.canShare({ text: 'probe', url: 'https://example.com' })
    : false;

  const nfc = 'NDEFReader' in window;
  const fileSystemAccess = 'showOpenFilePicker' in window;
  const clipboard = !!(navigator.clipboard?.writeText);

  return { webShare, nfc, fileSystemAccess, clipboard, timestamp: Date.now() };
}

Cache this result and expose it through a singleton or a React context value — calling canShare on every render is wasteful and produces misleading results when called outside a user gesture window.

Async Payload Formatting

Native OS bridges expect strictly typed data structures. Passing unvalidated or loosely typed objects to navigator.share or NDEFReader.write produces platform-specific silent failures that are extremely difficult to reproduce in development. Async payload formatting for native APIs covers the full serialisation contract for each API, including how to handle File objects, Blob coercion, and NDEF record type encoding.

The key rules:

  • Always pass a plain object literal to navigator.share — class instances with prototype methods will not serialise correctly across the OS boundary
  • NDEF records require explicit recordType and encoding fields; omitting them causes NDEFReader.write to throw DataError on some Android firmware versions
  • Validate that url values in a share payload are fully qualified (https://) — relative URLs cause DataError in Chrome
/**
 * Serialises a share payload to the strict structure navigator.share expects.
 * Strips keys with undefined/null values to avoid DataError on strict platforms.
 */
export function buildSharePayload({ title, text, url, files } = {}) {
  assertHardwarePrerequisites();

  const payload = {};
  if (title) payload.title = String(title);
  if (text)  payload.text  = String(text);
  if (url)   payload.url   = String(url);
  if (files?.length) payload.files = files;

  if (!navigator.canShare(payload)) {
    throw new DOMException(
      `Share payload is not supported on this platform: ${JSON.stringify(Object.keys(payload))}`,
      'DataError'
    );
  }
  return payload;
}

Progressive Fallback Routing

Progressive fallback routing for native device APIs Decision tree showing how to route a share action: first attempt native share, then clipboard copy, then reveal a manual URL input as the final fallback. User triggers share navigator.canShare(payload)? navigator.clipboard available? navigator.share(payload) → OS share sheet clipboard.writeText() → "Copied!" toast Reveal URL input → manual copy No Yes Yes No

Design fallback chains before writing the happy path. The decision tree above captures the standard routing logic: attempt native sharing, fall back to clipboard copy, then reveal a manual URL input as the last resort. This pattern appears in every production implementation because the capability matrix shifts dramatically across iOS WebViews, Chrome on Android, and desktop browsers.

When implementing File System Access API read and write workflows, the fallback for showOpenFilePicker is a conventional <input type="file"> element — which works everywhere, including browsers that will never implement the File System Access API. Keep the <input> in the DOM and hide it; show or hide the showOpenFilePicker path based on your capability matrix.

/**
 * Routes a share action through the three-tier fallback chain.
 * Resolves with a status string indicating which tier handled the action.
 */
export async function shareWithFallback(payload) {
  assertHardwarePrerequisites();

  try {
    if (navigator.canShare?.(payload)) {
      await navigator.share(payload);
      return { status: 'shared_via_native' };
    }

    if (navigator.clipboard?.writeText) {
      const text = payload.url ?? payload.text ?? '';
      await navigator.clipboard.writeText(text);
      return { status: 'copied_to_clipboard' };
    }

    return { status: 'fallback_unavailable' };
  } catch (error) {
    if (error.name === 'AbortError') {
      // User dismissed the share sheet — this is not an error
      return { status: 'user_cancelled' };
    }
    console.warn('Native integration failed:', error.name, error.message);
    return { status: 'error', errorName: error.name };
  }
}

For QR code generation for cross-device sharing as a fallback for Web NFC and share failures, generate QR codes client-side — they require no permissions and work in any browser.

What Each API Reaches The share API reaches installed applications, the clipboard reaches the system pasteboard, File System Access reaches user-chosen files, and Web NFC reaches physical tags. None substitutes for another. navigator.share installed apps and the OS share sheet — the only route to another application navigator.clipboard the system pasteboard, including rich HTML alongside plain text File System Access · Web NFC user-granted file handles that persist, and physical tags within a few centimetres

Web NFC

Web NFC: reading and writing tags in the browser is a Chrome-for-Android-only API. Always guard with 'NDEFReader' in window — the class is undefined on every desktop browser and on iOS regardless of Chrome version. NFC interactions require both a user gesture to initiate and an explicit "nfc" permission granted via the Permissions API.

/**
 * Writes a URL record to an NFC tag.
 * Requires Chrome for Android and the 'nfc' permission.
 */
export async function writeNfcTag(url) {
  assertHardwarePrerequisites();

  if (!('NDEFReader' in window)) {
    throw new DOMException(
      'Web NFC is only available in Chrome for Android.',
      'NotSupportedError'
    );
  }

  const writer = new NDEFReader();
  await writer.write({ records: [{ recordType: 'url', data: url }] });
  return { status: 'written' };
}

NFC permission denials surface as NotAllowedError. The Web NFC permission denied troubleshooting guide covers the full diagnostic flow for NotAllowedError cases, including the Android-side NFC settings that can prevent the browser from acquiring the hardware lock.

Clipboard API for Rich Content

Mastering the Clipboard API for rich text covers the full ClipboardItem API for writing structured HTML, images, and custom MIME types. navigator.clipboard.writeText covers the 90 % case; navigator.clipboard.write([new ClipboardItem({...})]) is required when your fallback needs to preserve formatting — for example, when copying formatted HTML to the clipboard without execCommand.

Safari requires that ClipboardItem data be synchronously constructed before the write call; wrapping Blob construction in a Promise breaks Safari’s user-gesture window. Chrome is more permissive. This is the primary platform divergence to account for when writing a clipboard fallback.

Cross-Browser Behaviour

API Chrome Android Chrome Desktop Safari iOS Safari macOS Firefox Edge
navigator.share ✓ full ✓ (89+, no files on older) ✓ (12.1+) ✓ (93+)
navigator.canShare ✓ (89+) ✓ (12.1+) ✓ (93+)
NDEFReader ✓ (89+)
showOpenFilePicker ✓ (86+) ✓ (86+)
clipboard.writeText ✓ (63+)
clipboard.write (rich) ✓ (13.1+) ✓ (13.1+)

The browser support matrix for the Web Share API provides version-by-version breakdown including the exact build numbers where file sharing support landed on each platform.

How Much Permission Each One Costs Sharing and copying ride on a user gesture and need no prompt. File System Access asks the user to pick a file. Web NFC raises a real permission prompt and can be denied permanently. gesture only — share, clipboard write no prompt, no stored decision, nothing to recover from user picks a file — File System Access the picker is the permission; a handle can be re-granted later an explicit prompt — Web NFC, which the user can deny for good

Error Handling Reference

All hardware APIs reject with DOMException. Match on error.name, not error.message — message strings are not standardised across browsers.

Exception name Cause Recovery
AbortError User dismissed the share sheet or file picker Treat as cancellation, not failure; do not log as error
NotAllowedError Called outside a user gesture, permission denied, or secure context missing Show permission denial recovery UI; do not re-prompt immediately
DataError Payload structure invalid or MIME type not in platform allowlist Log payload keys, validate with canShare(), strip unsupported fields
NotSupportedError API not available in this browser or device lacks hardware Route to next fallback tier
SecurityError Page is not a secure context Redirect to HTTPS; this is a deployment issue, not a runtime error

For structured permission denial recovery including implementing exponential backoff for permission re-prompts, store denial timestamps in localStorage and enforce a minimum cooldown before the next prompt — browsers may auto-deny rapid re-prompts regardless of your UI.

Common Pitfalls

  • Invoking hardware APIs outside user-initiated gestures. Browsers enforce a short user-activation window. Always bind API triggers to click or touchstart handlers — not setTimeout, requestAnimationFrame, or lifecycle hooks.
  • Assuming uniform MIME type support across iOS and Android. Mobile WebViews enforce divergent allowlists. Call navigator.canShare(payload) before constructing the payload, not after.
  • NFC polling on non-Android browsers. Web NFC is Chrome for Android only. Guard with 'NDEFReader' in window before any NFC initialisation.
  • Wrapping ClipboardItem blobs in Promise on Safari. Safari requires synchronous Blob construction inside the clipboard.write call. Async blob construction breaks the user-gesture window.
  • Failing to handle AbortError when users dismiss permission prompts. Native dialogs return AbortError on dismissal. Catch this explicitly and treat it as user cancellation, not an error.
  • Not caching the capability matrix. Calling navigator.canShare() repeatedly outside a user gesture can return inconsistent results. Resolve capabilities once at initialisation.

Choosing Between These APIs

The four capability families in this section overlap less than they appear to, and picking the wrong one produces a feature that works in development and disappoints in production.

Reach for the share sheet when the destination is another application and the user should choose it. Nothing else on the web can hand a file to an arbitrary installed app, and no list you maintain will match the apps a given person actually uses. Its cost is that you control almost nothing about the experience once the sheet opens.

Reach for the clipboard when the destination is unknown or the content is text. It has no destination list at all, which is exactly its strength: the user decides where the content lands, and every application on the device is a valid target. Rich text can carry both an HTML and a plain-text flavour so a formatted editor and a bare text field each receive something sensible.

Reach for File System Access when the user owns the file and will come back to it. A stored handle turns a web application into something that edits a document in place rather than downloading a copy each time — but it is Chromium-only, so the input-and-download path is not a fallback, it is the primary route for a large share of users.

Reach for Web NFC only when physical proximity is the point. It is the narrowest of the four by a wide margin: one browser, one platform, an explicit permission that can be denied permanently, and a payload measured in bytes. A tag is a pointer to content, never a container for it.

The common thread across all four is that capability must be resolved before the interface is drawn. Each has a detection gate, each has a fallback that must be designed rather than assumed, and each fails in a way the user should never have to interpret.

A Shared Discipline Across Every API

The four capability families differ in almost every detail, yet the code that uses them well looks remarkably similar. Five habits account for most of that similarity, and adopting them once pays across all of them.

Resolve capability before you draw. Every one of these APIs can be absent, present-but-restricted, or present-and-refusing-this-payload. Deciding which of those applies during render, rather than after a tap, is what allows a control to describe its own behaviour honestly. A button labelled Share that copies a link is not a graceful fallback; it is a broken promise that happened to do something.

Keep the gesture window clean. Sharing, clipboard writes, file pickers and NFC scans all require transient user activation, and all of them lose it to an intervening await. The structural fix is the same everywhere: do the expensive work when the content changes, hold the result, and let the handler contain a single asynchronous call. This one rule eliminates the largest single class of production failure across the whole section.

Treat metadata as load-bearing. A File without a MIME type, a clipboard item without a plain-text flavour, an NDEF record without a declared type — each produces a payload that technically transmits and practically fails at the far end. The receiving side has nothing but the metadata to dispatch on, so it is not decoration.

Expect a late refusal. canShare returning true, a picker returning a handle, and a permission being granted are all statements about the moment they were made. The target can still refuse the files, the handle’s permission can lapse between sessions, and the tag can be out of range. Every flow needs a branch for a failure that arrives after the user has committed.

Report outcomes without reporting content. These APIs carry the user’s own material — documents, photos, links, notes. Instrumentation belongs on the shape of the event and the name of the error, never on the values. That constraint is easiest to honour if it is built in from the first version rather than retrofitted after the analytics dashboard already exists.

None of these are specific to a single API, which is why they are stated here rather than repeated in each guide below. The guides deal with the parts that genuinely differ: the detection gate, the payload shape, the failure names, and the fallback that has to be designed rather than assumed.

The guides below are ordered roughly by how often each capability appears in a real product rather than by API complexity.

Payload formatting comes first because every other guide depends on it: a File with the right name and type, a ClipboardItem with the right flavours, a serialised record with the right fields. Get that layer wrong and each downstream API fails in its own confusing way.

File and media sharing follows, because attachments are where the share API stops being simple. Support is per payload rather than per browser, size limits are enforced by an application you cannot query, and the most common bug — a Blob where a File was required — produces an error message that names neither.

The clipboard guide is the one to read if only one of these matters to you. It has the widest reach of anything in this section, the lowest permission cost, and it is the fallback that every other guide eventually routes to.

File System Access and Web NFC come last deliberately. Both are powerful, both are Chromium-only, and both are best treated as enhancements over a route that already works for everyone else.

A Note on Longevity

These APIs move at different speeds. The share and clipboard interfaces have been stable for years and are safe to build directly against. File System Access and Web NFC are newer, narrower, and still shipping behaviour changes, so the code that touches them is worth isolating behind a small module of your own rather than scattering across a codebase.

The isolation costs almost nothing today and turns a future breaking change into one file to update rather than a search across every component that happened to call the API directly. It also makes the unsupported path easy to simulate, which is the same discipline the testing guides in the other sections argue for.

FAQ

Why do hardware APIs require a secure context?

Secure contexts — HTTPS or localhost — prevent man-in-the-middle attacks and ensure sensitive device data is only accessed by verified origins. Browsers cannot guarantee hardware isolation over unencrypted connections. The secure context requirements guide covers how mixed-content scenarios affect window.isSecureContext even when the page URL starts with https://.

How should I handle permission denials gracefully?

Wrap API calls in try/catch blocks, catch AbortError silently, and route users to a fallback UI that explains the limitation without breaking the core application flow. Provide clear recovery paths — clipboard fallbacks, QR codes, or manual URL inputs — rather than disabling functionality. The handling permission denials gracefully guide covers UX patterns for each denial scenario.

Can I use navigator.share on desktop browsers?

Yes, with caveats. Chrome on Windows, macOS, and Linux (89+) and Safari on macOS (12.1+) both support navigator.share. Firefox does not. File sharing support on desktop is more limited than on mobile — particularly for large files. Always use navigator.canShare() to validate the specific payload before showing the share button. The browser support matrix has the complete per-version breakdown.

How do I test hardware APIs in local development?

Serve over localhost — browsers treat localhost as a secure context even without TLS. For device-specific APIs like Web NFC, you need a physical Android device running Chrome. The step-by-step guide to testing Web Share API on localhost covers DevTools configuration, port forwarding for mobile device testing, and how to simulate permission states.