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.
| Dimension | WebView | Host-owned runtime |
|---|---|---|
| File picker | App must implement onShowFileChooser; default cancels | Unified chooser, one implementation |
| Camera capture | capture=camera ignored in in-app browsers | Uniform capture intent, no downgrade |
| Multi-file | Android multi-select and iOS cumulative need custom code | Native multi-select and cumulative capture |
| Permission gates | Manifest, runtime, scoped storage — all manual | Centralized, enforced once |
| Downloads | Needs setDownloadListener or nothing happens | Managed store, automatic |
| Test surface | Per-wrapper behavior, works in Safari lies | One 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
- Android Developers — WebChromeClient.onShowFileChooser: the default behavior is that WebView will cancel all file requests — the documented reason a bare WebView file input is inert.
- Android Developers — Build web apps in WebView: file access, permissions, and the onShowFileChooser requirement — the full set of gates an embedding app must open.
- Apple Developer Forums — iOS WKWebView resets FileList to one item on every camera capture (persists through iOS 26.4) — a confirmed, version-spanning WebKit limitation.
- web.dev — Web on Android: WebView delegates sensitive behavior to the embedding app — why upload and download are app-side features.
- Stack Overflow — Android WebView input type=file requires four separate hidden callback methods — the practical scope of the boilerplate.