Getting started
Load URLs and HTML, navigate the history stack, evaluate JavaScript, and intercept requests from Compose.
In this tutorial, you'll open a URL in a multiplatform WebView, react to loading state, navigate the history stack, evaluate JavaScript, and intercept navigations. Every snippet drops into a Compose Multiplatform commonMain source set.
Before you start
Add the dependency described in the WebView overview:
commonMain.dependencies {
implementation("dev.nucleusframework:composewebview:1.0.1")
}On desktop, open the window with Nucleus Tao (nucleusApplication and decorated-window-tao). The WebView embeds through NativeView and does not run on the AWT backend.
Load a URL
Hoist a WebViewState with rememberWebViewState and place WebView in the layout:
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.nucleusframework.webview.web.WebView
import dev.nucleusframework.webview.web.rememberWebViewState
@Composable
fun Browser(url: String) {
val state = rememberWebViewState(url) {
customUserAgentString = "MyApp/1.0"
}
WebView(state = state, modifier = Modifier.fillMaxSize())
}The trailing lambda on rememberWebViewState is a WebSettings receiver — set user agent, zoom, JavaScript, and platform-specific options there.
Load HTML
For inline HTML, use rememberWebViewStateWithHTMLData. For a file packaged with the app, use rememberWebViewStateWithHTMLFile:
val htmlState = rememberWebViewStateWithHTMLData(
data = """
<html><body><h1>Hello</h1></body></html>
""".trimIndent(),
baseUrl = null,
)
val fileState = rememberWebViewStateWithHTMLFile(
fileName = "index.html",
readType = WebViewFileReadType.ASSET_RESOURCES,
)WebViewFileReadType.ASSET_RESOURCES reads platform asset roots; COMPOSE_RESOURCE_FILES reads Compose Multiplatform resources.
React to loading state
WebViewState exposes snapshot state you can read in any composable — isLoading, loadingState, lastLoadedUrl, pageTitle, and errorsForCurrentRequest:
Box(Modifier.fillMaxSize()) {
WebView(state = state, modifier = Modifier.fillMaxSize())
if (state.isLoading) {
LinearProgressIndicator(
progress = {
(state.loadingState as? LoadingState.Loading)?.progress ?: 0f
},
modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter),
)
}
state.pageTitle?.let { title ->
Text(title, Modifier.align(Alignment.BottomCenter))
}
}LoadingState is a sealed class: Initializing, Loading(progress), or Finished.
Navigate with a navigator
Hoist a WebViewNavigator when you need toolbar actions or programmatic loads:
val state = rememberWebViewState("https://example.com")
val navigator = rememberWebViewNavigator()
Column(Modifier.fillMaxSize()) {
Row {
IconButton(
onClick = { navigator.navigateBack() },
enabled = navigator.canGoBack,
) { Icon(Icons.Default.ArrowBack, contentDescription = "Back") }
IconButton(
onClick = { navigator.navigateForward() },
enabled = navigator.canGoForward,
) { Icon(Icons.Default.ArrowForward, contentDescription = "Forward") }
IconButton(onClick = { navigator.reload() }) {
Icon(Icons.Default.Refresh, contentDescription = "Reload")
}
IconButton(onClick = {
navigator.loadUrl("https://kotlinlang.org")
}) { Text("Kotlin") }
}
WebView(state = state, navigator = navigator, modifier = Modifier.weight(1f))
}Navigator commands include loadUrl, loadHtml, loadHtmlFile, evaluateJavaScript, navigateBack, navigateForward, reload, and stopLoading.
Evaluate JavaScript
navigator.evaluateJavaScript("document.title") { title ->
println("Page title: $title")
}Bridge JavaScript to Kotlin
Pass a WebViewJsBridge to WebView. Handlers register under a method name; the page calls window.kmpJsBridge.callNative(...) after load:
val navigator = rememberWebViewNavigator()
val bridge = rememberWebViewJsBridge(navigator)
LaunchedEffect(bridge) {
bridge.register(object : IJsMessageHandler {
override fun methodName() = "echo"
override fun handle(
message: JsMessage,
navigator: WebViewNavigator?,
callback: (String) -> Unit,
) {
callback(message.params)
}
})
}
WebView(
state = state,
navigator = navigator,
webViewJsBridge = bridge,
modifier = Modifier.fillMaxSize(),
)From the page:
window.kmpJsBridge.callNative("echo", JSON.stringify({ hello: "world" }), function (result) {
console.log(result);
});Intercept navigations
Attach a RequestInterceptor when creating the navigator. It runs only for navigator-initiated main-frame loads — not for sub-resources:
val navigator = rememberWebViewNavigator(
requestInterceptor = object : RequestInterceptor {
override fun onInterceptUrlRequest(
request: WebRequest,
navigator: WebViewNavigator,
): WebRequestInterceptResult {
if (request.url.contains("blocked.example")) {
return WebRequestInterceptResult.Reject
}
return WebRequestInterceptResult.Allow
}
},
)Return Allow, Reject, or Modify(request) to rewrite the URL or headers.
Manage cookies
scope.launch {
state.cookieManager.setCookie(
url = "https://example.com",
cookie = Cookie(name = "session", value = "abc", domain = "example.com"),
)
val cookies = state.cookieManager.getCookies("https://example.com")
state.cookieManager.removeCookies("https://example.com")
state.cookieManager.removeAllCookies()
}Desktop notes
- Linux needs WebKit2GTK available on the system (
libwebkit2gtk). - Windows needs the WebView2 Runtime or a recent Edge install.
- macOS uses the system WKWebView; no extra runtime package is required.
- Open the host window on Tao. See DecoratedWindow on Tao.
The repository's e2e hosts (e2e-desktop, e2e-android, e2e-wasmJs, iosApp) run the same visual suite against a real WebView on each platform. Use them as a complete reference for wiring.
What's next
- API reference — every public type, its parameters, and defaults.
- WebView overview — supported targets and platform backends.
- Native views on Tao — how desktop embeds the native surface.