A file picker that opens instantly in Chrome stays inert inside a WebView. Android's WebView cancels every file request by default unless the embedding app implements WebChromeClient.onShowFileChooser (and the legacy openFileChooser on older versions); miss the callback, the runtime permission, or the activity result, and the tap does nothing — no error, no picker. On iOS, WKWebView silently downgrades capture=camera to the photo library inside in-app browsers, and a long-standing WebKit behavior resets the FileList to one item on every camera capture. These are app-side features, not web-side ones, so upload works is a property of the wrapper, not the page. A host-owned runtime ships one unified file and camera pipeline that behaves identically on every device and OS version.

The tap that does nothing

On Android, an <input type="file"> does not open a picker on its own. The platform routes it to WebChromeClient.onShowFileChooser, documented as the callback invoked when the web page has requested to either upload or save a file. The documentation is blunt about the default: WebView will cancel all file requests. So in a wrapper that has not implemented the callback, the input renders normally, the user taps it, and nothing happens at all. No error, no picker — the page looks completely healthy. On Android 4.x the legacy openFileChooser is a separate, hidden-API override, and forgetting to call the callback when the picker is cancelled leaves the input dead until the page reloads.

Three gates before one file

Even after the callback exists, a single upload still needs three more things the page cannot supply: the manifest permission (READ_EXTERNAL_STORAGE or camera), the Android runtime permission (API 23+), and correct handling of the activity result plus scoped storage (Android 10+). Miss any one and the failure is silent — a black screen, an empty files list, or a blob URL the page cannot read. This is why file upload does not work in my WebView is one of the most repeated WebView support questions: the control looks like a standard HTML element and fails like a missing native feature.

iOS: capture lies and the one-photo FileList

On iOS, WKWebView does handle file inputs, but the capture attribute is unreliable inside embedded browsers such as WeChat, TikTok, and Instagram — capture=camera is ignored and the user is dumped into the photo library. Worse, on iOS the system picker replaces the entire FileList on every session, so a web app can never accumulate multiple camera photos across taps; Apple's own developer forums confirm the behavior persists through iOS 26.4. Camera and microphone access through getUserMedia (WebRTC) was unsupported in WKWebView before iOS 14.3, and even after requires a delegate, entitlements, and a secure context.

What you lose without a unified I/O layer

  • Consistent uploads. The same <input type="file"> works in Chrome and fails in your app, because the picker is app-side code.
  • Camera capture. capture=camera means different things on different WebViews; in-app browsers silently downgrade it.
  • Multi-file flows. Android multi-select and iOS cumulative capture both need custom code the page cannot provide.
  • Downloads. A WebView does not save files by itself; without setDownloadListener a PDF or CSV link does nothing.
  • A real test surface. Works in Safari proves nothing, because every wrapper is a different implementation of the same HTML.

The runtime fix

A host-owned runtime provides one file and media pipeline below the page. It implements the chooser once for every OS and device, maps the accept attribute and camera or library intents uniformly, handles multi-select and cumulative capture so the FileList behaves as the spec intends, bridges downloads to a managed store, and applies the secure-context and permission rules centrally. The page calls the standard <input type="file"> and capture; the runtime guarantees the result. No per-app boilerplate, no silent dead inputs, no works in Safari surprises.

DimensionWebViewHost-owned runtime
File pickerApp must implement onShowFileChooser; default cancelsUnified chooser, one implementation
Camera capturecapture=camera ignored in in-app browsersUniform capture intent, no downgrade
Multi-fileAndroid multi-select and iOS cumulative need custom codeNative multi-select and cumulative capture
Permission gatesManifest, runtime, scoped storage — all manualCentralized, enforced once
DownloadsNeeds setDownloadListener or nothing happensManaged store, automatic
Test surfacePer-wrapper behavior, works in Safari liesOne runtime, one behavior

The file input is the most ordinary control on the web and the one most likely to be silently broken inside a WebView. It breaks not because the page is wrong but because the picker lives in the app, not the browser. A runtime moves that capability back below the page, so a tap that should open a picker always opens a picker.

Sources