From fullstack-team
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/fullstack-team:kotlin-webThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Patterns for building web frontends in Kotlin. Three approaches with clear selection criteria.
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 |
npx claudepluginhub andvl1/claude-plugin --plugin fullstack-teamGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.