
Embed web content with a controller-driven WebView offering navigation, JavaScript evaluation, app-page messaging, cookie and profile management, user scripts, navigation interception, and configurable headers/user-agent.
A Kotlin Multiplatform WebView for Compose.
It uses the browser engine already available on each platform. It does not bundle Chromium, WebKit, Firefox, or another browser engine into your app.
For most apps, add only webview-compose to commonMain:
kotlin {
sourceSets.commonMain.dependencies {
implementation("dev.brahmkshatriya.webview:webview-compose:<version>")
}
}You do not need to add each platform implementation yourself. Gradle selects the implementations needed by your targets.
For a simple page:
WebView(
url = "https://example.com",
modifier = Modifier.fillMaxSize(),
)Use a controller when you need navigation, state, JavaScript, cookies, profiles, or messaging:
@Composable
fun Browser() {
val controller = rememberWebViewController(
config = WebViewConfig(
initialUrl = "https://example.com",
),
)
WebView(
controller = controller,
modifier = Modifier.fillMaxSize(),
)
}rememberWebViewController closes the WebView automatically when it leaves the composition.
Navigation:
controller.loadUrl("https://example.com")
controller.reload()
controller.stopLoading()
controller.goBack()
controller.goForward()Load HTML:
controller.loadHtml(
html = "<h1>Hello</h1>",
baseUrl = "https://example.com",
)Read state in Compose:
val state by controller.collectState()
Text(state.title.orEmpty())
if (state.isLoading) {
LinearProgressIndicator(progress = { state.progress })
}The state also includes the current URL and back/forward availability.
From a coroutine:
when (val result = controller.evaluateJavaScript("document.title")) {
is JavaScriptResult.Value -> println(result.json)
is JavaScriptResult.Error -> println(result.message)
}Useful navigation helpers:
controller.loadUrl("https://example.com/login")
val callbackUrl = controller.awaitUrl {
it.startsWith("https://example.com/callback")
}
controller.awaitLoaded()For event-style handling, collect controller.events.
Profiles let you choose how browser data is shared and persisted:
WebViewConfig(
profile = WebViewProfile.Default,
)Available modes:
WebViewProfile.Default
WebViewProfile.Ephemeral
WebViewProfile.Persistent("account-a")Default uses the backend's normal app-owned browser session.Ephemeral keeps the session isolated and disposable.Persistent(name) gives you a stable isolated profile for that name.Named persistent profiles are supported by WPE WebKit, Linux Chromium/Firefox, Windows WebView2, Android WebView when AndroidX WebKit multi-profile support is available, and Apple WKWebView on iOS 17+ / macOS 14+.
Use the cookie jar attached to the controller's profile:
controller.cookies.set(
url = "https://example.com",
cookie = WebViewCookie(
name = "session",
value = "value",
secure = true,
httpOnly = true,
),
)
val cookies = controller.cookies.get("https://example.com")
controller.cookies.clear()If your app requires cookie access, request it when creating the controller:
val controller = rememberWebViewController(
requiredCapabilities = setOf(WebViewCapability.Cookies),
)Android may not expose all cookie metadata when reading cookies, so some fields can be null. The JavaScript iframe backend does not provide native cookie access.
On supported native backends, page JavaScript can send a string to the app:
window.webviewKmp.postMessage("ready")Receive it in Kotlin:
controller.messages.collect { message ->
println(message.data)
}Send a message back:
controller.postMessage("refresh")The page can receive app messages through window.webviewKmp.onmessage or the webview-kmp-message window event.
Use a navigation handler for callback URLs, custom schemes, or links that should leave the WebView:
val controller = rememberWebViewController(
config = WebViewConfig(
initialUrl = loginUrl,
navigationHandler = WebViewNavigationHandler { request ->
when {
request.url.startsWith(callbackUrl) ->
WebViewNavigationDecision.Cancel
request.url.startsWith("mailto:") ->
WebViewNavigationDecision.OpenExternally
else ->
WebViewNavigationDecision.Allow
}
},
),
requiredCapabilities = setOf(
WebViewCapability.NavigationInterception,
),
)Navigation interception is currently provided by Android WebView, WKWebView, WPE WebKit, and WebView2.
Install JavaScript that should run on each page:
val controller = rememberWebViewController(
config = WebViewConfig(
initialUrl = "https://example.com",
userScripts = listOf(
WebViewUserScript(
source = "window.myApp = { ready: true }",
injectionTime = WebViewUserScriptInjectionTime.DocumentStart,
),
),
),
)Request WebViewCapability.UserScripts when this is required.
Custom user agent:
WebViewConfig(
userAgent = UserAgent.Custom("MyApp"),
)Disable JavaScript:
WebViewConfig(
javaScript = JavaScriptMode.Disabled,
)Custom navigation headers where supported:
controller.loadUrl(
url = "https://example.com",
headers = mapOf("X-App-Version" to "1"),
)When a feature is mandatory, list it in requiredCapabilities. The library will avoid selecting a backend that cannot provide it.
| Platform | Backend |
|---|---|
| Android | Android WebView |
| iOS | WKWebView |
| macOS | WKWebView |
| Windows | Microsoft Edge WebView2 |
| Linux | WPE WebKit by default; Chromium/Firefox are optional |
| JavaScript | iframe |
WebViewBackendId.System means WPE WebKit on Linux.
To prefer an installed browser:
val controller = rememberWebViewController(
order = listOf(
WebViewBackendId.Chromium,
WebViewBackendId.Firefox,
),
)Common Chromium- and Firefox-based installations are detected automatically.
For unusual locations:
WEBVIEW_KMP_CHROMIUM_EXECUTABLE=/path/to/chromium
WEBVIEW_KMP_FIREFOX_EXECUTABLE=/path/to/firefoxChromium/Firefox backends use webview-kmp-owned profiles and do not reuse or modify the user's normal browser profile.
Windows uses the installed Edge WebView2 Runtime.
Your final app also needs WebView2Loader.dll next to the executable. The Windows Maven artifact publishes the loader with the webview2-loader classifier.
When building this repository, this helper copies the loader and license into one directory:
./gradlew copyWindowsWebView2LoaderiOS and macOS use the system WKWebView. Named persistent profiles require iOS 17+ or macOS 14+. Ephemeral profiles use WebKit's non-persistent data store.
The JS target uses an iframe, so normal browser same-origin restrictions apply. Cross-origin page contents, native cookies, and arbitrary cross-origin script injection are not available.
The common API includes:
Not yet exposed as public cross-platform APIs:
Most apps should use webview-compose.
If you are building your own UI integration, use webview-core plus the platform modules you need:
webview-core
webview-android
webview-ios
webview-js
webview-linux
webview-macos
webview-windows
Linux Chromium and Firefox have runtime integration tests. WPE WebKit is compile/link tested in this library. Android, JavaScript, Windows, iOS, and macOS compile successfully; broader real-device coverage is still ongoing on some platforms.
A Kotlin Multiplatform WebView for Compose.
It uses the browser engine already available on each platform. It does not bundle Chromium, WebKit, Firefox, or another browser engine into your app.
For most apps, add only webview-compose to commonMain:
kotlin {
sourceSets.commonMain.dependencies {
implementation("dev.brahmkshatriya.webview:webview-compose:<version>")
}
}You do not need to add each platform implementation yourself. Gradle selects the implementations needed by your targets.
For a simple page:
WebView(
url = "https://example.com",
modifier = Modifier.fillMaxSize(),
)Use a controller when you need navigation, state, JavaScript, cookies, profiles, or messaging:
@Composable
fun Browser() {
val controller = rememberWebViewController(
config = WebViewConfig(
initialUrl = "https://example.com",
),
)
WebView(
controller = controller,
modifier = Modifier.fillMaxSize(),
)
}rememberWebViewController closes the WebView automatically when it leaves the composition.
Navigation:
controller.loadUrl("https://example.com")
controller.reload()
controller.stopLoading()
controller.goBack()
controller.goForward()Load HTML:
controller.loadHtml(
html = "<h1>Hello</h1>",
baseUrl = "https://example.com",
)Read state in Compose:
val state by controller.collectState()
Text(state.title.orEmpty())
if (state.isLoading) {
LinearProgressIndicator(progress = { state.progress })
}The state also includes the current URL and back/forward availability.
From a coroutine:
when (val result = controller.evaluateJavaScript("document.title")) {
is JavaScriptResult.Value -> println(result.json)
is JavaScriptResult.Error -> println(result.message)
}Useful navigation helpers:
controller.loadUrl("https://example.com/login")
val callbackUrl = controller.awaitUrl {
it.startsWith("https://example.com/callback")
}
controller.awaitLoaded()For event-style handling, collect controller.events.
Profiles let you choose how browser data is shared and persisted:
WebViewConfig(
profile = WebViewProfile.Default,
)Available modes:
WebViewProfile.Default
WebViewProfile.Ephemeral
WebViewProfile.Persistent("account-a")Default uses the backend's normal app-owned browser session.Ephemeral keeps the session isolated and disposable.Persistent(name) gives you a stable isolated profile for that name.Named persistent profiles are supported by WPE WebKit, Linux Chromium/Firefox, Windows WebView2, Android WebView when AndroidX WebKit multi-profile support is available, and Apple WKWebView on iOS 17+ / macOS 14+.
Use the cookie jar attached to the controller's profile:
controller.cookies.set(
url = "https://example.com",
cookie = WebViewCookie(
name = "session",
value = "value",
secure = true,
httpOnly = true,
),
)
val cookies = controller.cookies.get("https://example.com")
controller.cookies.clear()If your app requires cookie access, request it when creating the controller:
val controller = rememberWebViewController(
requiredCapabilities = setOf(WebViewCapability.Cookies),
)Android may not expose all cookie metadata when reading cookies, so some fields can be null. The JavaScript iframe backend does not provide native cookie access.
On supported native backends, page JavaScript can send a string to the app:
window.webviewKmp.postMessage("ready")Receive it in Kotlin:
controller.messages.collect { message ->
println(message.data)
}Send a message back:
controller.postMessage("refresh")The page can receive app messages through window.webviewKmp.onmessage or the webview-kmp-message window event.
Use a navigation handler for callback URLs, custom schemes, or links that should leave the WebView:
val controller = rememberWebViewController(
config = WebViewConfig(
initialUrl = loginUrl,
navigationHandler = WebViewNavigationHandler { request ->
when {
request.url.startsWith(callbackUrl) ->
WebViewNavigationDecision.Cancel
request.url.startsWith("mailto:") ->
WebViewNavigationDecision.OpenExternally
else ->
WebViewNavigationDecision.Allow
}
},
),
requiredCapabilities = setOf(
WebViewCapability.NavigationInterception,
),
)Navigation interception is currently provided by Android WebView, WKWebView, WPE WebKit, and WebView2.
Install JavaScript that should run on each page:
val controller = rememberWebViewController(
config = WebViewConfig(
initialUrl = "https://example.com",
userScripts = listOf(
WebViewUserScript(
source = "window.myApp = { ready: true }",
injectionTime = WebViewUserScriptInjectionTime.DocumentStart,
),
),
),
)Request WebViewCapability.UserScripts when this is required.
Custom user agent:
WebViewConfig(
userAgent = UserAgent.Custom("MyApp"),
)Disable JavaScript:
WebViewConfig(
javaScript = JavaScriptMode.Disabled,
)Custom navigation headers where supported:
controller.loadUrl(
url = "https://example.com",
headers = mapOf("X-App-Version" to "1"),
)When a feature is mandatory, list it in requiredCapabilities. The library will avoid selecting a backend that cannot provide it.
| Platform | Backend |
|---|---|
| Android | Android WebView |
| iOS | WKWebView |
| macOS | WKWebView |
| Windows | Microsoft Edge WebView2 |
| Linux | WPE WebKit by default; Chromium/Firefox are optional |
| JavaScript | iframe |
WebViewBackendId.System means WPE WebKit on Linux.
To prefer an installed browser:
val controller = rememberWebViewController(
order = listOf(
WebViewBackendId.Chromium,
WebViewBackendId.Firefox,
),
)Common Chromium- and Firefox-based installations are detected automatically.
For unusual locations:
WEBVIEW_KMP_CHROMIUM_EXECUTABLE=/path/to/chromium
WEBVIEW_KMP_FIREFOX_EXECUTABLE=/path/to/firefoxChromium/Firefox backends use webview-kmp-owned profiles and do not reuse or modify the user's normal browser profile.
Windows uses the installed Edge WebView2 Runtime.
Your final app also needs WebView2Loader.dll next to the executable. The Windows Maven artifact publishes the loader with the webview2-loader classifier.
When building this repository, this helper copies the loader and license into one directory:
./gradlew copyWindowsWebView2LoaderiOS and macOS use the system WKWebView. Named persistent profiles require iOS 17+ or macOS 14+. Ephemeral profiles use WebKit's non-persistent data store.
The JS target uses an iframe, so normal browser same-origin restrictions apply. Cross-origin page contents, native cookies, and arbitrary cross-origin script injection are not available.
The common API includes:
Not yet exposed as public cross-platform APIs:
Most apps should use webview-compose.
If you are building your own UI integration, use webview-core plus the platform modules you need:
webview-core
webview-android
webview-ios
webview-js
webview-linux
webview-macos
webview-windows
Linux Chromium and Firefox have runtime integration tests. WPE WebKit is compile/link tested in this library. Android, JavaScript, Windows, iOS, and macOS compile successfully; broader real-device coverage is still ongoing on some platforms.