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.isSecureContextto betrue; serve overhttps://orlocalhost - Feature detection precedes invocation — check for the existence of
navigator.share,NDEFReader,showOpenFilePicker, ornavigator.clipboardbefore 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
recordTypeandencodingfields; omitting them causesNDEFReader.writeto throwDataErroron some Android firmware versions - Validate that
urlvalues in a share payload are fully qualified (https://) — relative URLs causeDataErrorin 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
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.
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.
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
clickortouchstarthandlers — notsetTimeout,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 windowbefore any NFC initialisation. - Wrapping
ClipboardItemblobs inPromiseon Safari. Safari requires synchronousBlobconstruction inside theclipboard.writecall. Async blob construction breaks the user-gesture window. - Failing to handle
AbortErrorwhen users dismiss permission prompts. Native dialogs returnAbortErroron 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.
What to Read Next
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.