
Cross-platform WebView UI and Playwright-style browser automation with AXTree extraction, CDP-based physical clicks, CSP-safe element location, anti-detection interactions, headless operation and screenshot capture.
Work in Progress — APIs are subject to change without notice. iOS and Android platforms have not been tested.
English | 简体中文
KBrowser is a Kotlin Multiplatform library that provides:
KBWebView — A cross-platform WebView UI component for Android, iOS, and Desktop (JVM). It is a pure WebView abstraction with a unified API similar to WKWebView / Android WebView.KBPage — A Playwright-inspired browser automation wrapper around KBWebView for Desktop (JVM). Built on Chrome DevTools Protocol (CDP), it provides AXTree extraction, CSP-safe element location, anti-detection physical clicks, screenshot capture, and coroutine-based thread safety.| Platform | KBWebView UI | KBPage Automation | Test Status |
|---|---|---|---|
| Desktop (JVM) | ✅ | ✅ Primary target | ✅ Actively tested |
| Android | ✅ | ❌ Not tested | |
| iOS | ✅ | ❌ Not tested |
Automation features (AXTree, CDP-based interactions, screenshots) are Desktop-only. On Android and iOS,
KBLocatorfalls back to JS injection.
Must use JetBrains Runtime (JBR) with JCEF. Standard JDK will not work. The library uses JCEF directly from JBR — JCEF is not bundled.
Distribution: JetBrains Runtime
Package: JDK + JCEF
| Platform | Minimum Version |
|---|---|
| Android | API 34 (Android 14) |
| iOS | iOS 17.0+ |
In gradle/libs.versions.toml:
[versions]
kbrowser = "0.1.0-alpha31"
[libraries]
kbrowser = { module = "io.github.lzdev42:kbrowser", version.ref = "kbrowser" }In your module's build.gradle.kts:
implementation(libs.kbrowser)Configure your IDE or build tool to use JBR with JCEF as the project runtime. In compose.desktop configuration, the following JVM arguments are required:
compose.desktop {
application {
jvmArgs += listOf(
"--enable-native-access=jcef",
"--add-opens=jcef/com.jetbrains.cef.remote.browser=ALL-UNNAMED",
"--add-opens=jcef/com.jetbrains.cef.remote=ALL-UNNAMED"
)
}
}Note: Without these JVM arguments, OSR mode will not support Chinese input.
On JVM, JCEF supports two rendering modes. The mode is determined at initialization time via KBrowser.initializeConfig(useOsr = ...) and cannot be changed after the application starts.
| Mode | useOsr |
Overlay Compose UI | Event Handling | Performance |
|---|---|---|---|---|
| Non-OSR (Native Window) | false |
❌ Cannot overlay Compose UI on top of JCEF | ✅ Normal | ✅ Better |
| OSR (Off-Screen Rendering) | true |
✅ Can overlay Compose UI on top of JCEF | Lower (higher CPU/GPU usage) |
Known Issue (OSR mode): In OSR mode, JCEF renders off-screen, allowing Compose UI to be layered on top. However, mouse and keyboard events are received by the underlying JCEF native view, not by the Compose overlay. This means interactive Compose components placed over the JCEF area will not respond to user input. This issue has not been investigated yet and is currently low priority.
Recommendation: Use non-OSR mode (useOsr = false) unless you specifically need to overlay Compose UI on top of the browser. Non-OSR mode provides better rendering performance and correct event handling for the JCEF view itself.
KBrowser.initializeConfig() and initializeKBrowser() must be called before application {}:
import xyz.kbrowser.webview.KBrowser
import xyz.kbrowser.webview.initializeKBrowser
import androidx.compose.ui.window.application
fun main() {
// 1. Configure cache directory and rendering mode (must be called once at startup)
KBrowser.initializeConfig(
storageDir = "/path/to/cache",
useOsr = false // Set to true only if you need Compose UI overlay on top of JCEF
)
// 2. Initialize JCEF engine (suspend function, must be called before any UI)
kotlinx.coroutines.runBlocking {
initializeKBrowser()
}
// 3. Start Compose application
application {
Window(onCloseRequest = ::exitApplication) { App() }
}
}KBWebView is a pure WebView component. Use it when you need to display web content in your Compose UI:
@Composable
fun BrowserScreen() {
val webView = rememberKBWebView(initialUrl = "https://example.com")
LaunchedEffect(webView) {
webView.onNewWindowRequest = { url ->
webView.loadUrl(url)
}
}
Column(Modifier.fillMaxSize()) {
KBWebView(webView = webView, modifier = Modifier.weight(1f))
Row {
Button(onClick = { webView.goBack() }) { Text("←") }
Button(onClick = { webView.goForward() }) { Text("→") }
Button(onClick = { webView.reload() }) { Text("↺") }
}
}
}KBPage is a coroutine-based automation wrapper around KBWebView. It provides:
loadUrl suspends until page finishes loading)Mutex for writes and @Volatile for readsval page = KBrowser.newPage(url = "https://example.com")
page.onNewPage = { url -> println("New page request: $url") }
page.loadUrl("https://example.com/login")
// Coordinate mode (physical events, anti-detection)
page.getByLabel("Username").fill("admin")
page.getByLabel("Password").type("secret")
page.getByRole("button", name = "Login").click()
// JS mode (DOM event simulation, bypasses occlusion)
page.getByLabel("Username").jsFill("admin")
page.getByLabel("Password").jsType("secret")
page.getByRole("button", name = "Login").jsClick()
// AXTree extraction
val tree = page.getRawAxTree().getCleanedAxTree()
println("Visible nodes: ${tree.visibleElements}")
// Screenshot
val png = page.screenshot()
page.close()suspend methods of KBPage internally switch to Dispatchers.Main via withContext, so they can be called from any coroutine context.KBPage node cache uses Mutex for write serialization and @Volatile for read visibility. Read operations (e.g., click) will never deadlock with write operations (e.g., getRawAxTree).getCleanedAxTree, getViewportAxTree) are pure Kotlin extension functions that execute in the caller's coroutine context without switching threads.KBrowser.newPage() creates a headless page by instantiating JvmWebView with isHeadless = true. The implementation creates a transparent JFrame (opacity = 0, 1280×800) and mounts the JCEF component on it.
Limitations:
useOsr = true).Xvfb) is required.Apache License 2.0 — see LICENSE.
Portions of the JVM/Desktop implementation are derived from IntelliJ IDEA (JetBrains s.r.o.), licensed under Apache 2.0. Modified files retain original copyright notices.
Work in Progress — APIs are subject to change without notice. iOS and Android platforms have not been tested.
English | 简体中文
KBrowser is a Kotlin Multiplatform library that provides:
KBWebView — A cross-platform WebView UI component for Android, iOS, and Desktop (JVM). It is a pure WebView abstraction with a unified API similar to WKWebView / Android WebView.KBPage — A Playwright-inspired browser automation wrapper around KBWebView for Desktop (JVM). Built on Chrome DevTools Protocol (CDP), it provides AXTree extraction, CSP-safe element location, anti-detection physical clicks, screenshot capture, and coroutine-based thread safety.| Platform | KBWebView UI | KBPage Automation | Test Status |
|---|---|---|---|
| Desktop (JVM) | ✅ | ✅ Primary target | ✅ Actively tested |
| Android | ✅ | ❌ Not tested | |
| iOS | ✅ | ❌ Not tested |
Automation features (AXTree, CDP-based interactions, screenshots) are Desktop-only. On Android and iOS,
KBLocatorfalls back to JS injection.
Must use JetBrains Runtime (JBR) with JCEF. Standard JDK will not work. The library uses JCEF directly from JBR — JCEF is not bundled.
Distribution: JetBrains Runtime
Package: JDK + JCEF
| Platform | Minimum Version |
|---|---|
| Android | API 34 (Android 14) |
| iOS | iOS 17.0+ |
In gradle/libs.versions.toml:
[versions]
kbrowser = "0.1.0-alpha31"
[libraries]
kbrowser = { module = "io.github.lzdev42:kbrowser", version.ref = "kbrowser" }In your module's build.gradle.kts:
implementation(libs.kbrowser)Configure your IDE or build tool to use JBR with JCEF as the project runtime. In compose.desktop configuration, the following JVM arguments are required:
compose.desktop {
application {
jvmArgs += listOf(
"--enable-native-access=jcef",
"--add-opens=jcef/com.jetbrains.cef.remote.browser=ALL-UNNAMED",
"--add-opens=jcef/com.jetbrains.cef.remote=ALL-UNNAMED"
)
}
}Note: Without these JVM arguments, OSR mode will not support Chinese input.
On JVM, JCEF supports two rendering modes. The mode is determined at initialization time via KBrowser.initializeConfig(useOsr = ...) and cannot be changed after the application starts.
| Mode | useOsr |
Overlay Compose UI | Event Handling | Performance |
|---|---|---|---|---|
| Non-OSR (Native Window) | false |
❌ Cannot overlay Compose UI on top of JCEF | ✅ Normal | ✅ Better |
| OSR (Off-Screen Rendering) | true |
✅ Can overlay Compose UI on top of JCEF | Lower (higher CPU/GPU usage) |
Known Issue (OSR mode): In OSR mode, JCEF renders off-screen, allowing Compose UI to be layered on top. However, mouse and keyboard events are received by the underlying JCEF native view, not by the Compose overlay. This means interactive Compose components placed over the JCEF area will not respond to user input. This issue has not been investigated yet and is currently low priority.
Recommendation: Use non-OSR mode (useOsr = false) unless you specifically need to overlay Compose UI on top of the browser. Non-OSR mode provides better rendering performance and correct event handling for the JCEF view itself.
KBrowser.initializeConfig() and initializeKBrowser() must be called before application {}:
import xyz.kbrowser.webview.KBrowser
import xyz.kbrowser.webview.initializeKBrowser
import androidx.compose.ui.window.application
fun main() {
// 1. Configure cache directory and rendering mode (must be called once at startup)
KBrowser.initializeConfig(
storageDir = "/path/to/cache",
useOsr = false // Set to true only if you need Compose UI overlay on top of JCEF
)
// 2. Initialize JCEF engine (suspend function, must be called before any UI)
kotlinx.coroutines.runBlocking {
initializeKBrowser()
}
// 3. Start Compose application
application {
Window(onCloseRequest = ::exitApplication) { App() }
}
}KBWebView is a pure WebView component. Use it when you need to display web content in your Compose UI:
@Composable
fun BrowserScreen() {
val webView = rememberKBWebView(initialUrl = "https://example.com")
LaunchedEffect(webView) {
webView.onNewWindowRequest = { url ->
webView.loadUrl(url)
}
}
Column(Modifier.fillMaxSize()) {
KBWebView(webView = webView, modifier = Modifier.weight(1f))
Row {
Button(onClick = { webView.goBack() }) { Text("←") }
Button(onClick = { webView.goForward() }) { Text("→") }
Button(onClick = { webView.reload() }) { Text("↺") }
}
}
}KBPage is a coroutine-based automation wrapper around KBWebView. It provides:
loadUrl suspends until page finishes loading)Mutex for writes and @Volatile for readsval page = KBrowser.newPage(url = "https://example.com")
page.onNewPage = { url -> println("New page request: $url") }
page.loadUrl("https://example.com/login")
// Coordinate mode (physical events, anti-detection)
page.getByLabel("Username").fill("admin")
page.getByLabel("Password").type("secret")
page.getByRole("button", name = "Login").click()
// JS mode (DOM event simulation, bypasses occlusion)
page.getByLabel("Username").jsFill("admin")
page.getByLabel("Password").jsType("secret")
page.getByRole("button", name = "Login").jsClick()
// AXTree extraction
val tree = page.getRawAxTree().getCleanedAxTree()
println("Visible nodes: ${tree.visibleElements}")
// Screenshot
val png = page.screenshot()
page.close()suspend methods of KBPage internally switch to Dispatchers.Main via withContext, so they can be called from any coroutine context.KBPage node cache uses Mutex for write serialization and @Volatile for read visibility. Read operations (e.g., click) will never deadlock with write operations (e.g., getRawAxTree).getCleanedAxTree, getViewportAxTree) are pure Kotlin extension functions that execute in the caller's coroutine context without switching threads.KBrowser.newPage() creates a headless page by instantiating JvmWebView with isHeadless = true. The implementation creates a transparent JFrame (opacity = 0, 1280×800) and mounts the JCEF component on it.
Limitations:
useOsr = true).Xvfb) is required.Apache License 2.0 — see LICENSE.
Portions of the JVM/Desktop implementation are derived from IntelliJ IDEA (JetBrains s.r.o.), licensed under Apache 2.0. Modified files retain original copyright notices.