KBrowser
0.1.0-alpha38indexedCross-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.
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.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"
)
}
}
⚠️ Important: Without these JVM arguments, OSR mode will not support Chinese/CJK text input (English input is unaffected). In OSR mode, JCEF renders off-screen with no native window handling IME. Chinese input relies on reflective calls to JCEF internal classes, and
--add-opensgrants access to those classes. Without them, IME events are silently dropped, but English works via key events — easy to misdiagnose as "IME broken" rather than "missing configuration". In non-OSR mode, JCEF uses a native window where IME is handled natively by the OS, so no special arguments are needed.
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.
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.
Chinese Input in OSR Mode: Besides the JVM arguments above, OSR mode also requires focus synchronization for Chinese input — KBrowser handles this internally, no user action needed. For technical details, see the Architecture Document.
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, correct event handling, and Chinese input without extra configuration.
This project includes a full demo application showcasing all KBrowser features.
Desktop: On launch, you first choose a rendering mode (OSR / Non-OSR) — this is desktop-specific, since OSR mode has lower performance and requires extra JVM arguments for Chinese input. After selecting OSR, the main page offers:
Selecting Non-OSR directly shows a WebGL scene (demonstrating the limitation that Compose UI cannot be overlaid in Non-OSR mode).
Mobile: No rendering mode selection (mobile WebView has no OSR concept), goes directly to the feature list. The 6 WebView component demo pages share code with the desktop. The browser automation page shows a warning that some features may not work on mobile.
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()
}
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 readsKBrowser provides two distinct page-creation APIs, each with a clear single responsibility:
KBrowser.newPage(profile: KBProfile? = null) — Creates a UI page for display in a Compose window via the KBWebView Composable. Render size is determined by the Compose .Both APIs only create the page; navigation is done via page.loadUrl(url), which is a suspend function that returns when loading completes:
val page = KBrowser.newHeadlessTab() // create
page.loadUrl("https://example.com") // navigate (suspend)
val png = page.screenshot() // ready
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.
| Platform | KBWebView UI | KBPage Automation | Test Status |
|---|
| Desktop (JVM) | ✅ | ✅ Primary target | ✅ Actively tested |
| Android | ✅ | ⚠️ Partial (JS fallback) | ❌ Not tested |
| iOS | ✅ | ⚠️ Partial (JS fallback) | ❌ Not tested |
| Mode | useOsr | Overlay Compose UI | Event Handling | Performance | Chinese Input |
|---|
| Non-OSR (Native Window) | false | ❌ Cannot overlay Compose UI on top of JCEF | ✅ Normal | ✅ Better | ✅ Native support |
| OSR (Off-Screen Rendering) | true | ✅ Can overlay Compose UI on top of JCEF | ⚠️ Events are dispatched to the underlying JCEF view, not to overlay Compose components | Lower (higher CPU/GPU usage) | ⚠️ Requires JVM args |
val page = KBrowser.newPage()
page.onNewPage = { url -> println("New page request: $url") }
page.loadUrl("https://example.com")
// 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.snapshot().rawTree.getCleanedAxTree()
println("Visible nodes: ${tree.visibleElements}")
// Get page snapshot (YAML + raw data from the same fetch)
val result = page.snapshot(SnapshotMode.VIEWPORT)
val yaml = result.yaml // For AI
val rawTree = result.rawTree // Raw data, refids consistent with yaml
// 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).AxTreeData.getCleanedAxTree(), AxTreeData.toYamlSnapshot()) are pure Kotlin extension functions that execute in the caller's coroutine context without switching threads. getCleanedAxTree() actually filters nodes within the current viewport (same viewport-range logic as toYamlSnapshot(VIEWPORT)).modifierKBrowser.newHeadlessTab(profile: KBProfile? = null, viewportWidth = 1280, viewportHeight = 720) — Creates a headless page for background automation (screenshots, CDP operations, AX Tree extraction). Render size is determined by a transparent JFrame (opacity = 0) that hosts the JCEF component. Never mount a headless page onto the KBWebView Composable — it will cause size anomalies.Surfaced from shared tags and platforms — no rankings paid for.