Compose Multiplatform UI library for WebAssembly/JS target - declarative framework for sharing UIs across multiple platforms with Kotlin.
—
Compose Multiplatform for WASM/JS provides seamless integration with browser APIs and web platform features. This enables access to browser-specific functionality, system preferences, network operations, and JavaScript interoperability while maintaining the declarative Compose programming model.
Direct access to browser window and document objects.
// Browser window access
val window: Window = kotlinx.browser.window
val document: Document = kotlinx.browser.documentWindow Operations:
@Composable
fun WindowIntegration() {
LaunchedEffect(Unit) {
// Window properties
val windowWidth = window.innerWidth
val windowHeight = window.innerHeight
// Browser information
val userAgent = window.navigator.userAgent
val language = window.navigator.language
val platform = window.navigator.platform
// Location and history
val currentUrl = window.location.href
window.history.pushState(null, "New Page", "/new-path")
}
}Document Manipulation:
@Composable
fun DocumentIntegration() {
LaunchedEffect(Unit) {
// Document properties
val title = document.title
document.title = "New Title"
// Element access
val element = document.getElementById("myElement")
val canvas = document.querySelector("canvas")
// DOM manipulation
val newElement = document.createElement("div")
newElement.textContent = "Created from Kotlin"
document.body?.appendChild(newElement)
}
}fun isSystemInDarkTheme(): Boolean {
return window.matchMedia("(prefers-color-scheme: dark)").matches
}
@Composable
fun DarkModeAwareTheme(content: @Composable () -> Unit) {
var isDarkMode by remember { mutableStateOf(isSystemInDarkTheme()) }
LaunchedEffect(Unit) {
val mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
val listener: (MediaQueryListEvent) -> Unit = { event ->
isDarkMode = event.matches
}
mediaQuery.addEventListener("change", listener)
// Cleanup handled automatically
}
MaterialTheme(
colors = if (isDarkMode) darkColors() else lightColors(),
content = content
)
}fun getCurrentLanguage(): String {
return window.navigator.languages.firstOrNull()
?: window.navigator.language
?: "en"
}
fun getCurrentLocale(): String {
val language = getCurrentLanguage()
return language.split("-").first() // Extract primary language
}
@Composable
fun LocalizedContent() {
val currentLanguage = remember { getCurrentLanguage() }
val isRightToLeft = remember {
listOf("ar", "he", "fa", "ur").contains(currentLanguage.split("-").first())
}
CompositionLocalProvider(
LocalLayoutDirection provides if (isRightToLeft) LayoutDirection.Rtl else LayoutDirection.Ltr
) {
// Content automatically adapts to language direction
Text(stringResource(Res.string.welcome_message))
}
}fun getDevicePixelRatio(): Double {
return window.devicePixelRatio
}
fun prefersReducedMotion(): Boolean {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches
}
fun prefersHighContrast(): Boolean {
return window.matchMedia("(prefers-contrast: high)").matches
}
@Composable
fun AccessibilityAwareUI() {
val reducedMotion = remember { prefersReducedMotion() }
val highContrast = remember { prefersHighContrast() }
AnimatedVisibility(
visible = shouldShowContent,
enter = if (reducedMotion) EnterTransition.None else fadeIn(),
exit = if (reducedMotion) ExitTransition.None else fadeOut()
) {
Surface(
color = if (highContrast) Color.Black else MaterialTheme.colors.surface
) {
// Content adapted for accessibility
}
}
}Modern web API for network requests.
suspend fun fetchData(url: String): String {
return window.fetch(url).await().text().await()
}
suspend fun fetchJson(url: String): dynamic {
val response = window.fetch(url).await()
if (!response.ok) {
throw Exception("Network request failed: ${response.status}")
}
return response.json().await()
}
@Composable
fun NetworkDataLoader() {
var data by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
isLoading = true
try {
data = fetchData("https://api.example.com/data")
error = null
} catch (e: Exception) {
error = e.message
data = null
} finally {
isLoading = false
}
}
when {
isLoading -> CircularProgressIndicator()
error != null -> Text("Error: $error", color = MaterialTheme.colors.error)
data != null -> Text("Data: $data")
}
}Traditional HTTP requests with more control.
suspend fun xmlHttpRequest(url: String, method: String = "GET"): String {
return suspendCoroutine { continuation ->
val xhr = XMLHttpRequest()
xhr.open(method, url)
xhr.onload = {
if (xhr.status == 200.toShort()) {
continuation.resume(xhr.responseText)
} else {
continuation.resumeWithException(
Exception("HTTP ${xhr.status}: ${xhr.statusText}")
)
}
}
xhr.onerror = {
continuation.resumeWithException(Exception("Network error"))
}
xhr.send()
}
}object BrowserStorage {
fun setItem(key: String, value: String) {
window.localStorage.setItem(key, value)
}
fun getItem(key: String): String? {
return window.localStorage.getItem(key)
}
fun removeItem(key: String) {
window.localStorage.removeItem(key)
}
fun clear() {
window.localStorage.clear()
}
}
@Composable
fun PersistentSettings() {
var theme by remember {
val saved = BrowserStorage.getItem("theme") ?: "light"
mutableStateOf(saved)
}
LaunchedEffect(theme) {
BrowserStorage.setItem("theme", theme)
}
Switch(
checked = theme == "dark",
onCheckedChange = { isDark ->
theme = if (isDark) "dark" else "light"
}
)
}object SessionStorage {
fun setItem(key: String, value: String) {
window.sessionStorage.setItem(key, value)
}
fun getItem(key: String): String? {
return window.sessionStorage.getItem(key)
}
}fun getCurrentPlatform(): String = "Web WASM"
fun getBrowserInfo(): BrowserInfo {
val userAgent = window.navigator.userAgent
return BrowserInfo(
name = detectBrowserName(userAgent),
version = detectBrowserVersion(userAgent),
isMobile = isMobileDevice(userAgent),
supportsWebAssembly = supportsWasm(),
supportsWebGL = supportsWebGL()
)
}
data class BrowserInfo(
val name: String,
val version: String,
val isMobile: Boolean,
val supportsWebAssembly: Boolean,
val supportsWebGL: Boolean
)
fun detectBrowserName(userAgent: String): String {
return when {
"Chrome" in userAgent -> "Chrome"
"Firefox" in userAgent -> "Firefox"
"Safari" in userAgent -> "Safari"
"Edge" in userAgent -> "Edge"
else -> "Unknown"
}
}
@Composable
fun PlatformAwareUI() {
val browserInfo = remember { getBrowserInfo() }
if (browserInfo.isMobile) {
// Mobile-optimized UI
Column(
modifier = Modifier.padding(8.dp)
) {
// Larger touch targets
Button(
onClick = { /* action */ },
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
) {
Text("Mobile Button")
}
}
} else {
// Desktop-optimized UI
Row {
// More compact layout
Button(onClick = { /* action */ }) {
Text("Desktop Button")
}
}
}
}// Define external JavaScript functions
@JsFun("(message) => { console.log(message); }")
external fun jsConsoleLog(message: String)
@JsFun("(callback) => { setTimeout(callback, 1000); }")
external fun jsSetTimeout(callback: () -> Unit)
@JsFun("() => { return Date.now(); }")
external fun jsGetTimestamp(): Double
@Composable
fun JavaScriptIntegration() {
LaunchedEffect(Unit) {
// Call JavaScript functions
jsConsoleLog("Hello from Kotlin!")
val timestamp = jsGetTimestamp()
println("Current timestamp: $timestamp")
jsSetTimeout {
println("JavaScript timeout callback executed")
}
}
}@Composable
fun JavaScriptObjectAccess() {
LaunchedEffect(Unit) {
// Access global JavaScript objects
val console = window.asDynamic().console
console.log("Direct console access")
// Use browser APIs
val performance = window.asDynamic().performance
val navigationStart = performance.timing.navigationStart
// Access custom JavaScript objects
val customObject = window.asDynamic().myCustomObject
if (customObject != null) {
customObject.customMethod("parameter")
}
}
}@Composable
fun URLManagement() {
var currentPath by remember { mutableStateOf(window.location.pathname) }
LaunchedEffect(Unit) {
// Listen for navigation events
window.addEventListener("popstate") { event ->
currentPath = window.location.pathname
}
}
fun navigateTo(path: String) {
window.history.pushState(null, "", path)
currentPath = path
}
when (currentPath) {
"/" -> HomeScreen()
"/about" -> AboutScreen()
"/settings" -> SettingsScreen()
else -> NotFoundScreen()
}
BottomNavigation {
BottomNavigationItem(
icon = { Icon(Icons.Default.Home, contentDescription = null) },
label = { Text("Home") },
selected = currentPath == "/",
onClick = { navigateTo("/") }
)
BottomNavigationItem(
icon = { Icon(Icons.Default.Info, contentDescription = null) },
label = { Text("About") },
selected = currentPath == "/about",
onClick = { navigateTo("/about") }
)
}
}fun parseUrlParameters(): Map<String, String> {
val params = mutableMapOf<String, String>()
val search = window.location.search.removePrefix("?")
if (search.isNotEmpty()) {
search.split("&").forEach { param ->
val (key, value) = param.split("=", limit = 2)
params[decodeURIComponent(key)] = decodeURIComponent(value)
}
}
return params
}
@Composable
fun URLParameterHandler() {
val urlParams = remember { parseUrlParameters() }
val userId = urlParams["userId"]
val tab = urlParams["tab"] ?: "profile"
userId?.let { id ->
UserProfile(userId = id, initialTab = tab)
} ?: run {
Text("User ID not provided")
}
}fun measurePerformance(): PerformanceData {
val performance = window.asDynamic().performance
val timing = performance.timing
return PerformanceData(
navigationStart = timing.navigationStart as Double,
domContentLoaded = timing.domContentLoadedEventEnd as Double,
loadComplete = timing.loadEventEnd as Double,
firstPaint = getFirstPaintTime(),
memory = getMemoryUsage()
)
}
data class PerformanceData(
val navigationStart: Double,
val domContentLoaded: Double,
val loadComplete: Double,
val firstPaint: Double?,
val memory: MemoryInfo?
)
@Composable
fun PerformanceMonitor() {
var performanceData by remember { mutableStateOf<PerformanceData?>(null) }
LaunchedEffect(Unit) {
delay(1000) // Wait for page to load
performanceData = measurePerformance()
}
performanceData?.let { data ->
Column {
Text("Load Time: ${data.loadComplete - data.navigationStart}ms")
Text("DOM Ready: ${data.domContentLoaded - data.navigationStart}ms")
data.memory?.let { memory ->
Text("Memory Used: ${memory.usedJSHeapSize / 1024 / 1024}MB")
}
}
}
}fun logToBrowserConsole(message: String, level: LogLevel = LogLevel.LOG) {
val console = window.asDynamic().console
when (level) {
LogLevel.LOG -> console.log(message)
LogLevel.INFO -> console.info(message)
LogLevel.WARN -> console.warn(message)
LogLevel.ERROR -> console.error(message)
}
}
enum class LogLevel { LOG, INFO, WARN, ERROR }
@Composable
fun DebuggingSupport() {
LaunchedEffect(Unit) {
// Enable debugging in development
if (isDebugMode()) {
logToBrowserConsole("App started in debug mode", LogLevel.INFO)
// Register global error handler
window.addEventListener("error") { event ->
val errorEvent = event as ErrorEvent
logToBrowserConsole(
"Global error: ${errorEvent.message} at ${errorEvent.filename}:${errorEvent.lineno}",
LogLevel.ERROR
)
}
}
}
}
fun isDebugMode(): Boolean {
return window.location.hostname == "localhost" ||
window.location.search.contains("debug=true")
}Install with Tessl CLI
npx tessl i tessl/maven-org-jetbrains-compose-ui--ui-wasm-js