Native in-app browser for React Native, powered by Nitro Modules.
Installation ยท Quick start ยท API ยท Options ยท FAQ ยท Changelog
| โก JSI bindings | Direct native calls through Nitro Modules. No JSON serialization, no scheduler hops. |
| ๐ฏ Right primitive per platform | SFSafariViewController on iOS, Chrome Custom Tabs on Android. Not a WKWebView reimplementation. |
| ๐ OAuth built in | openAuth wraps ASWebAuthenticationSession with ephemeral sessions and redirect interception. |
| ๐ช Hook + functions | useInAppBrowser() for React state, named exports for everything else. |
| ๐งฉ TypeScript first | Discriminated result types, as const enums, full JSDoc. |
| ๐ฆ Tree-shakeable | "sideEffects": false, ESM build, lazy native module init. |
| Minimum | Tested up to | |
|---|---|---|
| React Native | 0.75 (New Architecture) |
0.85 |
| iOS | 15.1 |
26.2 |
| Android | API 23 (Android 6) |
API 36 (Android 16) |
react-native-nitro-modules |
0.35 |
0.35.4 |
Important
This library requires the React Native New Architecture and does not work in Expo Go. Use Expo prebuild / dev clients instead.
yarn add react-native-inappbrowser-nitro react-native-nitro-modulesnpm / pnpm / bun
npm install react-native-inappbrowser-nitro react-native-nitro-modules
pnpm add react-native-inappbrowser-nitro react-native-nitro-modules
bun add react-native-inappbrowser-nitro react-native-nitro-modulescd ios && pod installAutolinking handles everything. No manual MainApplication edits.
Note
Release builds with ProGuard/R8 need this rule in android/app/proguard-rules.pro:
# react-native-inappbrowser-nitro
-keep class com.inappbrowsernitro.** { *; }Without it, you'll see Couldn't find class 'com/inappbrowsernitro/HybridInappbrowserNitro'.
import { useInAppBrowser } from 'react-native-inappbrowser-nitro/hooks'
function DocsButton() {
const { open, isLoading, error } = useInAppBrowser()
return (
<Pressable
disabled={isLoading}
onPress={() => open('https://nitro.margelo.com')}
>
<Text>{isLoading ? 'Openingโฆ' : 'Open docs'}</Text>
{error && <Text style={{ color: 'red' }}>{error.message}</Text>}
</Pressable>
)
}The hook guards state updates after unmount and returns stable open/openAuth references via useCallback.
import { isAvailable, open } from 'react-native-inappbrowser-nitro'
if (await isAvailable()) {
const result = await open('https://github.com', {
preferredBarTintColor: { light: '#FFFFFF', dark: '#000000' }, // iOS
toolbarColor: { light: '#FFFFFF', dark: '#000000' }, // Android
readerMode: true,
})
if (result.type === 'success') {
console.log('Opened', result.url)
}
}import { openAuth } from 'react-native-inappbrowser-nitro'
const result = await openAuth(
'https://example.com/oauth/authorize?client_id=โฆ&redirect_uri=myapp%3A%2F%2Fcb',
'myapp://cb',
{
ephemeralWebSession: true, // iOS: don't share Safari cookies
enableEdgeDismiss: false, // iOS: block swipe-to-dismiss during auth
forceCloseOnRedirection: true, // Android: close tab on redirect match
}
)
if (result.type === 'success' && result.url) {
const code = new URL(result.url).searchParams.get('code')
// exchange code for token
}All exports come from the package root unless noted. Every function returns a Promise.
| Export | Signature | Description |
|---|---|---|
isAvailable |
() => Promise<boolean> |
true when a compliant Safari/Custom Tabs runtime is reachable. Always true on iOS; on Android requires a Custom Tabsโcapable browser. |
open |
(url, options?) => Promise<InAppBrowserResult> |
Present an in-app browser. Resolves when the user dismisses or the system closes it. |
openAuth |
(url, redirectUrl, options?) => Promise<InAppBrowserAuthResult> |
Run an authentication session. Resolves the moment native code intercepts a navigation matching redirectUrl. |
close |
() => Promise<void> |
Dismiss the current browser. No-op when none is presented. |
closeAuth |
() => Promise<void> |
Cancel an in-flight openAuth session. |
useInAppBrowser |
() => UseInAppBrowserReturn |
Hook wrapping open/openAuth with isLoading + error state. Exported from react-native-inappbrowser-nitro/hooks. |
type BrowserResultType = 'cancel' | 'dismiss' | 'success'
interface InAppBrowserResult {
type: BrowserResultType
url?: string // final URL captured by the browser session
message?: string // human-readable reason on `dismiss`
}open and openAuth reject with an Error when the URL is empty, missing a scheme, or uses a denied scheme (javascript:, data:, vbscript:). These checks run in JS before the call crosses JSI.
open and openAuth accept one options object. Platform-only fields are ignored on the other platform.
| Option | Type | Default | Notes |
|---|---|---|---|
dismissButtonStyle |
'done' | 'close' | 'cancel' |
'done' |
Toolbar dismiss button label. |
preferredBarTintColor |
DynamicColor |
system | Safari toolbar background hint. iOS 26 Liquid Glass may ignore it. |
preferredControlTintColor |
DynamicColor |
system | Safari control tint hint. iOS 26 may adapt it for contrast. |
preferredStatusBarStyle |
'default' | 'lightContent' | 'darkContent' |
system | Status bar appearance while presented. |
readerMode |
boolean |
false |
iOS only. Ask Safari to enter Reader Mode if the page supports it; Android Custom Tabs ignore this option. |
animated |
boolean |
true |
Animate present/dismiss. |
modalPresentationStyle |
ModalPresentationStyle |
'automatic' |
UIKit modal style. |
modalTransitionStyle |
ModalTransitionStyle |
'coverVertical' |
UIKit transition. Use 'partialCurl' only with 'fullScreen'. |
modalEnabled |
boolean |
true |
Present modally instead of pushing onto a navigation stack. |
enableBarCollapsing |
boolean |
false |
Collapse toolbar on scroll. |
ephemeralWebSession |
boolean |
false |
openAuth only: do not persist cookies/credentials. |
enableEdgeDismiss |
boolean |
true |
Allow swipe-from-edge to dismiss. |
overrideUserInterfaceStyle |
'unspecified' | 'light' | 'dark' |
'unspecified' |
Force light/dark while presented. |
formSheetPreferredContentSize |
{ width, height } |
UIKit | Preferred form-sheet size. UIKit may adapt or ignore it on iPhone. |
| Option | Type | Default | Notes |
|---|---|---|---|
showTitle |
boolean |
false |
Show page title under the URL bar. |
toolbarColor |
DynamicColor |
browser default | Top toolbar background. |
secondaryToolbarColor |
DynamicColor |
browser default | Bottom toolbar background. |
navigationBarColor |
DynamicColor |
system | API 27+. |
navigationBarDividerColor |
DynamicColor |
system | API 28+. |
enableUrlBarHiding |
boolean |
false |
Hide URL bar on scroll. |
enableDefaultShare |
boolean |
false |
Show share menu item. Use shareState for finer control. |
shareState |
'default' | 'on' | 'off' |
'default' |
Override share menu visibility. |
colorScheme |
'system' | 'light' | 'dark' |
'system' |
Custom Tab theme hint. |
headers |
Record<string, string> |
{} |
HTTP headers on initial request. |
forceCloseOnRedirection |
boolean |
false |
Auto-close tab when redirect URL matches. |
hasBackButton |
boolean |
false |
Show back arrow instead of X. |
browserPackage |
string |
auto | Pin to a specific browser, e.g. com.android.chrome. |
showInRecents |
boolean |
true |
Keep the tab in Android Recents after closing. |
includeReferrer |
boolean |
false |
Send the host app package as Referrer. |
instantAppsEnabled |
boolean |
true |
Allow Instant Apps to handle the URL. |
enablePullToRefresh |
boolean |
false |
Enable swipe-to-refresh. |
enablePartialCustomTab |
boolean |
false |
Show a resizable bottom sheet on Android 13+. |
animations |
BrowserAnimations |
system | Custom enter/exit animation resource names. |
Color options accept a DynamicColor object:
interface DynamicColor {
base?: string // fallback
light?: string // light mode
dark?: string // dark mode
highContrast?: string // increased contrast, where supported
}Each value must be #RRGGBB or #AARRGGBB. Missing mode-specific values fall back to base, then the system default.
iOS 26 renders SFSafariViewController chrome with system Liquid Glass. Apple controls the final toolbar material, contrast, and legibility:
preferredBarTintColorcan have little or no visible effect.preferredControlTintColormay be adapted by the system.formSheetPreferredContentSizeis only a UIKit preference and is commonly adapted on iPhone.
The properties are still forwarded for iOS versions and contexts that honor them.
If pixel-exact browser chrome matters, use a WKWebView-based screen for non-auth flows. Do not use WKWebView for OAuth; it lacks Safari's process isolation, cookie sharing, autofill, and many providers forbid it.
Android prefers Chrome Custom Tabs. On devices without a Custom Tabsโcapable browser the system shows a chooser via Intent.ACTION_VIEW, and option fields like toolbarColor are silently ignored.
Why not use WKWebView / react-native-webview?
SFSafariViewController and Chrome Custom Tabs share the system Safari/Chrome session โ cookies, autofill, content blockers, and password autofill from iCloud Keychain / Google Password Manager. They run in a separate process from your app, so the host app cannot read page content. Most OAuth providers require this. WKWebView offers none of it.
Does it work with Expo?
Yes, in Expo prebuild / dev client projects. It does not work in Expo Go (managed workflow) because Nitro requires native compilation.
Can I use this with the Old Architecture?
No. Nitro Modules require the New Architecture (newArchEnabled=true on Android, Fabric/TurboModule autolinking on iOS).
"InAppBrowser is not available" on Android emulator
The default emulator image ships without a Custom Tabsโcapable browser. Install Chrome from the Play Store image, or use a Pixel system image with Play Services preinstalled.
Why does my OAuth flow open in Safari instead of in-app?
You're calling open instead of openAuth. openAuth uses ASWebAuthenticationSession, the only iOS API that can intercept a redirect URL programmatically. open uses SFSafariViewController, which cannot.
Result type is 'dismiss' right after I call open
The URL was rejected by the JS-side validator (empty / missing scheme / denied scheme). Check result.message for the reason. Native-side logs are also visible in Xcode / Logcat.
Contributions welcome. The library is small and well-tested โ a good place to land your first React Native PR.
Found a bug or have a feature request? Open an issue.
git clone https://github.com/mCodex/react-native-inappbrowser-nitro
cd react-native-inappbrowser-nitro
yarn install
yarn codegen # regenerate Nitro bindings + build
yarn typecheck
yarn lintRun the example app:
cd example
yarn ios # or: yarn androidA pre-commit hook (Husky + lint-staged + Biome) auto-formats staged files. CI runs on iOS (macos-26, Xcode 26.2) and Android (ubuntu-latest, JDK 21).