React Native WebView that auto-sizes to match its HTML content—whether you load local HTML or full external websites—without manual measurements, timers, or layout flicker.
Important
⚡️ SizedWebView keeps the parent scroll view in charge by disabling the inner WebView scroll and syncing height changes via a lightweight bridge.
Tip
💡 Works out-of-the-box with dynamic CMS pages, FAQs, marketing landers, local HTML snippets, or full external sites.
Three defaults changed in the 1.1.x line. Each is a one-line migration:
- Named import only. The default export was removed to keep tree-shaking predictable across bundlers.
- import SizedWebView from 'react-native-sized-webview'; + import { SizedWebView } from 'react-native-sized-webview';
originWhitelistnow defaults to['http://*', 'https://*']. Standard HTTP(S) navigation keeps working; non-web schemes (file:,javascript:,data:,intent:) are blocked by default. Tighten it for production if you only load a specific origin:<SizedWebView + originWhitelist={['https://your-trusted-domain.com']} source={{ uri: 'https://your-trusted-domain.com/page' }} />javaScriptEnabledis now respected. Passingfalsedisables auto-sizing; the container falls back tominHeight(orcontainerStyle.height). This unblocks rendering static HTML on iOS 26 (#3).
- 📐 Wrapper-based measurement keeps the WebView content in a dedicated container, so height always reflects the real DOM footprint.
- 🚀 Modern pipeline powered by
ResizeObserver,MutationObserver,visualViewport, and font-load events with graceful fallbacks. - 🖼 Media aware: images, iframes, and videos schedule immediate + next-frame re-measures as soon as they finish loading.
- 🧼 Auto-prunes trailing
<br>/empty<p>tags that CMS editors often append, eliminating phantom spacing. - 🛡️ Sanity guard clamps runaway heights and retries with the last good value, so flaky pages never lock your layout.
- 🧵 Keeps the WebView scroll-disabled so outer
ScrollViews and gesture handlers stay silky smooth. - 🎨 Transparent background by default; style the container however you like.
- ⚙️ Friendly API with
minHeight,containerStyle, andonHeightChangecallbacks. - 🌲 ESM-first build, fully typed,
sideEffects: falsefor optimal tree shaking. - 📱 Verified on iOS, Android, and Expo Go out of the box.
yarn add react-native-sized-webview react-native-webview
# or
npm install react-native-sized-webview react-native-webviewNo native steps are needed beyond the upstream react-native-webview dependency.
import { SizedWebView } from 'react-native-sized-webview';
const Article = () => (
<SizedWebView
minHeight={180}
source={{
html: `
<html>
<body>
<h1>Privacy policy</h1>
<p>Generated by your CMS and sized automatically ✨</p>
</body>
</html>
`,
}}
containerStyle={{ borderRadius: 12, overflow: 'hidden' }}
onHeightChange={(height) => console.log('content height', height)}
/>
);yarn
yarn example ios # or yarn example androidThe example showcases:
- Auto-sizing dynamic HTML with toggled sections.
- Live external sites (Marvel, NFL, Google, Wikipedia, The Verge) embedded without layout thrash.
- Real-time height readouts so you can verify your own endpoints quickly.
- One code path that works the same on iOS, Android, and Expo Go.
Note
🧪 The demo is built with Expo; swap the uri to test your own pages instantly.
| Prop | Type | Default | Description |
|---|---|---|---|
minHeight |
number |
0 |
Minimum height (dp) applied to the container. When 0, the container is unsized until the first measurement arrives (avoids layout flicker and the iOS 26 WKWebView 1px feedback loop). |
containerStyle |
StyleProp<ViewStyle> |
— | Styles applied to the wrapping View. Use it for padding, borders, or shadows. Do not set height — it is managed by the hook. |
onHeightChange |
(height: number) => void |
— | Callback fired whenever a new height is committed. Great for analytics or debugging. Never fires for invalid or out-of-range values. |
originWhitelist |
string[] |
['http://*', 'https://*'] |
Origins the WebView is allowed to navigate to. Blocks non-web schemes (file:, javascript:, data:, intent:) by default. Tighten it to a specific origin list for stricter environments. |
javaScriptEnabled |
boolean |
true |
When false, the auto-height bridge is not injected and the container falls back to minHeight. Use for static HTML that doesn't need JS. |
...WebViewProps |
— | — | All remaining props are forwarded to the underlying react-native-webview. User-supplied values always win over the defaults above. |
Note
🧩 scrollEnabled defaults to false so sizing remains deterministic. Only enable it if the WebView should manage its own scroll.
- Namespaced message protocol. The injected bridge posts values prefixed with
__RN_SIZED_WV__:and the hook rejects everything else, so your ownonMessagetraffic cannot accidentally (or maliciously) mutate the container height. - Safe-by-default origin list.
originWhitelistdefaults to['http://*', 'https://*']— HTTP(S) navigation works, but non-web schemes (file:,javascript:,data:,intent:) are blocked. Tighten to a specific origin for production apps that only load trusted content. - Respected JS toggle.
javaScriptEnabled={false}is honored; the bridge is not injected when you disable scripts. - Clamped heights. A shared
MAX_COMMITTED_HEIGHT(120 000 dp) caps both sides of the bridge to defend against runaway values from broken markup or third-party scripts. - No native code. This package ships only JavaScript/TypeScript — there is no Objective-C, Swift, Java, or Kotlin to audit.
- Warning. Never interpolate untrusted strings into
injectedJavaScriptorinjectedJavaScriptBeforeContentLoaded. Anything passed there runs inside the WebView page context and can reach React Native throughwindow.ReactNativeWebView.
- Trailing
<br>and empty<p>tags are stripped automatically so CMS exports don’t leave phantom padding. - Images, iframes, and videos reschedule measurements the moment they finish loading—perfect for hero images at the end of an article.
- Wrapper rebuild + fallback timers keep measurements stable even if the remote page rewrites the entire DOM after load.
- Measurements above safe bounds are retried and then clamped to the last known good height, protecting against broken markup or third-party scripts.
- Injected bridge re-parents all body children into a dedicated wrapper, trims trailing blanks, and observes DOM mutations, layout changes, font loads, and viewport shifts.
- Media events (images / iframes / video) trigger immediate + next-frame samples so late assets still report accurate heights.
- Media elements stay observed via
ResizeObserver+ decode promises, catching intrinsic size changes without duplicate network requests. - Height calculations are debounced via
requestAnimationFrameand a short idle timer to prevent resize storms. - Measurements arrive through
postMessage, thenuseAutoHeightcoalesces them into a single render per frame. - Package exports the bridge, hook, and helpers individually, making it easy to build bespoke wrappers when needed.
| Scenario | Plain react-native-webview |
react-native-sized-webview |
|---|---|---|
| Initial render layout shifts | Requires timers / manual height guesswork | Zero shifts; height resolved before paint |
| React state updates on content change | Manual postMessage plumbing |
Automatic bridge with RAF + debounce guard |
Scrolling in parent ScrollView |
Nested scroll can fight gestures | Parent retains full momentum and gesture priority |
Benchmarks were captured on CMS articles up to 3k words in a 60 fps RN dev build. The bridge batches DOM mutations so even long documents resize without thrashing the JS thread.
Every hot path is designed to run at its theoretical complexity floor — no allocations in steady state, no repeated DOM walks, and at most one forced layout per measurement frame.
| Hot path | Complexity | Notes |
|---|---|---|
Message parsing (useAutoHeight) |
O(1) | Namespaced-prefix check, single Number() coerce, constant-bound clamp. |
| Height commit (rAF-batched) | O(1) amortized per frame | Sub-pixel diffs are dropped; at most one React render per animation frame. |
| DOM mutation callback | O(added nodes) | Scans only each mutation's addedNodes, not the whole tree. Media elements are deduped via a WeakSet. |
measureHeight |
1 forced reflow / call | Reads the wrapper element only — its box is authoritative because every <body> child lives inside it. |
| Trailing-node prune DFS | Runs only when the DOM is dirty | A mutation-driven dirty flag skips the recursive walk on resize / font / viewport ticks when nothing structural changed. |
The net effect: resize storms, font loads, and viewport changes cost a single getBoundingClientRect() per frame — nothing more. Paired with sideEffects: false and named-only exports, the library stays fast and small in the final bundle.
- Ships as ESM-first (
lib/module/**) with"sideEffects": false. - Named exports only — no default export — so every bundler can drop what you don't use.
- Importing only
useAutoHeightorcomposeInjectedScriptdoes not pull the injected-bridge string into your bundle.
yarn testJest runs with full coverage collection and enforces 100% statements, branches, functions, and lines across the TypeScript source.
yarn
yarn lint
yarn typecheck
yarn test --watch=false
yarn example ios # or yarn example androidThis project uses react-native-builder-bob for packaging and release-it for publishing.
Caution
🔬 Before submitting PRs that touch the bridge script, please test the example app on both iOS and Android to catch edge cases with interactive embeds.
MIT © Mateus Andrade