SuperAPI AI Directory

kotlin-web

Frontend & websites

View repository

Updated 10-15 17:32

About this skill

Kotlin Web Frontend

Patterns for building web frontends in Kotlin. Three approaches with clear selection criteria.

Current Versions (May 2026)

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.886 or other floating wrapper versions; use the BOM pattern: implementation(platform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:2026.5.2")) then non-versioned dependencies.

Targets at a glance — do not confuse them

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.

Decision Tree

Selection rule: Compose WASM — for fast MVPs and internal tools. For production applications with serious requirements for UX, performance, and accessibility — Kotlin/JS + React or Vue.

Option 1: Compose for Web (WASM) — MVP / Internal Tools

Choose when:

  • Fast MVP or prototype with maximum reuse of mobile UI
  • Internal tools, admin panels, dashboards (limited audience)
  • Team is Kotlin-first, no JS/TS expertise and no plans to hire
  • Limitations acceptable: large bundle, no SEO, limited accessibility (a11y)

Don't choose when:

  • Production-facing product with external users
  • Need SSR/SEO
  • Full accessibility needed (screen readers, ARIA) — Compose WASM renders to canvas, DOM-based a11y is absent
  • Need extensive npm ecosystem (charting, rich text editors, maps)
  • Bundle size is critical (Compose WASM ~2-5MB initial)
  • Need to integrate into existing JS/TS app
  • Long-term support and frontend development expected

Limitations to be aware of:

  • Canvas-based rendering: no native DOM elements, no element inspector in the browser
  • Accessibility: practically absent (no screen reader support)
  • Mobile web: heavy bundle on mobile networks
  • Ecosystem maturity: Compose for Web is still in active development, API may change

Option 2: Kotlin/JS + React — Production Web Apps

Choose when:

  • Production-facing product, mature ecosystem needed
  • Need React ecosystem (existing components, npm libraries)
  • Team knows React patterns or willing to learn
  • Integrating Kotlin business logic into React frontend
  • Need SSR via Next.js
  • Full accessibility (a11y), SEO, performance needed

Don't choose when:

  • No React experience on team AND tight deadlines
  • Pure Kotlin stack preferred AND this is an internal tool (→ Compose WASM)

Option 3: Kotlin/JS + Vue — Production Web Apps

Choose when:

  • Production-facing product, Vue ecosystem preferred
  • Need Vue ecosystem (Vuetify, Quasar, PrimeVue)
  • Vue's reactivity model feels natural (similar to Kotlin Flow)
  • Single File Components (SFC) preferred
  • Team knows Vue patterns

Don't choose when:

  • No Vue experience on team
  • Need maximum community support (React has larger Kotlin/JS community)

Comparison Matrix

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

Compose WASM Setup

Project Structure

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

Entry Point

// 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.

Build Configuration — 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).

WASM Limitations

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)

Browser API Interop

// 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>

Reference: references/compose-wasm.md for advanced patterns

Kotlin/JS Common Setup

Both React and Vue approaches share these fundamentals:

Gradle Configuration

// 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"))
        }
    }
}

npm Interop Pattern

// 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>()
}

State Bridging (Kotlin Flow → JS Framework)

// 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
}

Reference: references/kotlin-js-react.md for React-specific patterns

Reference: references/kotlin-js-vue.md for Vue-specific patterns

Shared Code Strategy

What to Share Between Mobile and Web

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

Module Structure for Shared Code

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/

Build & Run Commands

# Compose WASM
./gradlew :composeApp:wasmJsBrowserDevelopmentRun      # Dev server
./gradlew :composeApp:wasmJsBrowserDistribution        # Production

# Kotlin/JS
./gradlew :webApp:jsBrowserDevelopmentRun              # Dev server
./gradlew :webApp:jsBrowserDistribution                # Production

Integration with Other Skills

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