skills/kotlin-web/SKILL.md
Kotlin web frontends — Compose WASM, Kotlin/JS + React, Kotlin/JS + Vue. Use for browser UI work with decision tree for framework choice and shared code strategy. Always reflect the current versions listed below; do not assume older Compose MP / kotlin-wrappers releases.
npx skillsauth add andvl1/claude-plugin kotlin-webInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for building web frontends in Kotlin. Three approaches with clear selection criteria.
| Component | Version | Notes |
|---|---|---|
| Compose Multiplatform | 1.10.3 | WASM target uses wasmJs("web"). Skiko canvas backend. |
| kotlin-wrappers | 2026.5.2 | Calendar versioning since 2024. Compatible with Kotlin 2.3.21. Inside: kotlin-react 19.x, kotlin-react-router 7.x, kotlin-emotion 11.x. Pin the BOM, not individual pre.NNN versions. |
| Kotlin | 2.3.21 | Same toolchain across JS / WASM. |
| Ktor (browser HTTP) | 3.4.3 | Use ktor-client-js for Kotlin/JS, ktor-client-core + ktor-client-js (or ktor-client-cio not on web) — pick ktor-client-js. |
Do not write
pre.886or other floating wrapper versions; use the BOM pattern:implementation(platform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:2026.5.2"))then non-versioned dependencies.
Two distinct Kotlin targets sit behind "web", and each picks a different framework family:
| Target | Gradle DSL | Framework family | Output |
|---|---|---|---|
| WASM | wasmJs("web") { browser() } | Compose Multiplatform (Skiko canvas) | Single .wasm + tiny JS shim. Canvas-rendered UI. |
| JS (IR) | js(IR) { browser() } | Kotlin/JS + React (or Vue) via kotlin-wrappers | DOM-native UI, full browser tooling. |
When Decision Tree picks Compose WASM → use wasmJs("web"). When it picks React/Vue → use js(IR). Do not try to put React on wasmJs or Compose for Web on js(IR) — different ecosystems entirely.
A KMP project can host both targets simultaneously (e.g. an internal admin via WASM and a public-facing site via JS+React); they are separate Gradle modules with separate build.gradle.kts files.
Правило выбора: Compose WASM — для быстрых MVP и внутренних инструментов. Для продакшн-приложений с серьёзными требованиями к UX, производительности и доступности — Kotlin/JS + React или Vue.
Choose when:
Don't choose when:
Limitations to be aware of:
Choose when:
Don't choose when:
Choose when:
Don't choose when:
| Criterion | Compose WASM | Kotlin/JS + React | Kotlin/JS + Vue | |-----------|-------------|-------------------|-----------------| | Maturity | Early (MVP/internal) | Production-ready | Production-ready | | Code sharing with mobile | Maximum | Business logic only | Business logic only | | Bundle size | ~2-5MB | ~200-500KB | ~200-500KB | | npm ecosystem | Limited | Full access | Full access | | SEO/SSR | No | Yes (Next.js) | Yes (Nuxt) | | Accessibility (a11y) | None (canvas) | Full (DOM) | Full (DOM) | | Learning curve (Kotlin dev) | Lowest | Medium | Medium | | Browser APIs | Via interop | Native | Native | | Performance (rendering) | Canvas-based | DOM-native | DOM-native | | Long-term maintainability | Risky (API changes) | Stable | Stable |
composeApp/
└── src/
├── commonMain/kotlin/ # Shared UI + logic
├── wasmJsMain/kotlin/ # Web entry point
│ ├── Main.kt
│ └── platform/
│ ├── WasmStorage.kt # localStorage
│ └── WasmPlatform.kt # Browser APIs
└── wasmJsMain/resources/
└── index.html
// wasmJsMain/kotlin/Main.kt
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
CanvasBasedWindow(canvasElementId = "ComposeTarget") {
App()
}
}
CanvasBasedWindow (from Compose MP, ExperimentalComposeUiApi) mounts Skiko canvas into DOM element by id. ID string MUST match <canvas id="ComposeTarget"> in wasmJsMain/resources/index.html — mismatch = blank page, no error. Conventional id: ComposeTarget.
wasmJs target// composeApp/build.gradle.kts
kotlin {
wasmJs("web") {
browser {
commonWebpackConfig {
outputFileName = "composeApp.js"
}
devServer {
port = 8080
// optional:
// open = true
// static = mutableListOf("$rootDir/composeApp/src/wasmJsMain/resources")
}
}
binaries.executable()
}
}
Target-name string ("web" in wasmJs("web")) sets Gradle source-set prefix — produces webMain / webTest source sets alongside default wasmJsMain. Older docs show wasmJs(IR) — deprecated; named-target form (wasmJs("web") or bare wasmJs { ... }) is now standard. Pick one convention per module and stick to it; mixing breaks source-set resolution.
devServer.port defaults to a free port — set explicitly to avoid clashes with other dev servers (Vite 5173, CRA 3000, Spring 8080 — careful). static list adds extra resource roots beyond wasmJsMain/resources (rarely needed; default already serves index.html).
| Limitation | Workaround |
|-----------|------------|
| No Room database | Use localStorage / IndexedDB via expect/actual |
| No Dispatchers.IO | Use Dispatchers.Default |
| CORS on all requests | Configure server or proxy |
| No file system | Use Blob URLs, File API |
| Canvas rendering | No DOM accessibility by default |
| Large bundle (~2-5MB) | Lazy loading, code splitting (limited) |
// Access browser APIs from WASM
import kotlinx.browser.window
import kotlinx.browser.document
fun getCurrentUrl(): String = window.location.href
fun setPageTitle(title: String) {
document.title = title
}
// Using external JS functions
external fun fetch(url: String): Promise<Response>
references/compose-wasm.md for advanced patternsBoth React and Vue approaches share these fundamentals:
// build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
kotlin {
js(IR) {
browser {
commonWebpackConfig {
cssSupport { enabled.set(true) }
}
}
binaries.executable()
}
sourceSets {
jsMain.dependencies {
// Shared Kotlin modules
implementation(projects.core.common)
implementation(projects.core.network)
// npm dependencies
implementation(npm("axios", "1.7.0"))
}
}
}
// Declare external npm module
@JsModule("axios")
@JsNonModule
external fun axios(config: dynamic): Promise<dynamic>
// Type-safe wrapper
suspend fun <T> httpGet(url: String): T {
val response = axios(js("{ method: 'get', url: url }")).await()
return response.data.unsafeCast<T>()
}
// Bridge Kotlin StateFlow to framework-observable state
// Common pattern for both React and Vue:
class StateBridge<T>(private val flow: StateFlow<T>) {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
fun subscribe(callback: (T) -> Unit): () -> Unit {
val job = scope.launch {
flow.collect { callback(it) }
}
return { job.cancel() }
}
fun currentValue(): T = flow.value
}
references/kotlin-js-react.md for React-specific patternsreferences/kotlin-js-vue.md for Vue-specific patterns| Layer | Share? | How |
|-------|--------|-----|
| Domain models | Yes | commonMain module |
| Repository interfaces | Yes | commonMain module |
| Use cases | Yes | commonMain module |
| Network client config | Mostly | expect/actual for engine |
| ViewState/ViewEvent | Compose WASM: Yes | Same sealed classes |
| ViewState/ViewEvent | Kotlin/JS: Adapt | Map to framework state |
| UI components | Compose WASM: Yes | Same composables |
| UI components | Kotlin/JS: No | Rewrite in React/Vue |
| Navigation | No | Platform-specific |
| Storage | Interface only | expect/actual |
project/
├── shared/
│ ├── core/ # Domain models, utils
│ │ └── src/commonMain/
│ ├── data/ # Repository interfaces, DTOs
│ │ └── src/commonMain/
│ └── domain/ # Use cases
│ └── src/commonMain/
│
├── composeApp/ # Compose WASM + mobile
│ └── src/
│ ├── commonMain/ # Shared Compose UI
│ ├── androidMain/
│ ├── iosMain/
│ └── wasmJsMain/
│
└── webApp/ # Kotlin/JS + React/Vue (if not using Compose WASM)
└── src/jsMain/
# Compose WASM
./gradlew :composeApp:wasmJsBrowserDevelopmentRun # Dev server
./gradlew :composeApp:wasmJsBrowserDistribution # Production
# Kotlin/JS
./gradlew :webApp:jsBrowserDevelopmentRun # Dev server
./gradlew :webApp:jsBrowserDistribution # Production
| Skill | Relationship |
|-------|-------------|
| kmp | Project structure, source sets, expect/actual patterns |
| kmp-feature-slice | Feature generation. Load kotlin-web-templates.md for web targets |
| compose-arch | Architecture rules apply to Compose WASM UI (same composables) |
| ktor-client | HTTP client patterns, shared between mobile and web |
development
Effective Go patterns — idiomatic code, testing, benchmarks, project layout. Always use Go 1.21+ patterns.
development
Go microservices — gRPC, REST, cloud-native patterns, service discovery, circuit breakers, observability, health checks, graceful shutdown.
development
Go concurrency mastery — goroutines, channels, context, sync primitives, patterns, performance.
testing
Android WorkManager for guaranteed background execution - use for deferred tasks, periodic syncs, file uploads, notifications, and task chains. Covers CoroutineWorker, constraints, chaining, testing, and troubleshooting. Use when implementing background work that needs reliable execution across app restarts and doze mode.