diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..6f2a4a6
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,17 @@
+on: [push]
+
+name: Build
+
+jobs:
+  build-ubuntu:
+    name: Commit release
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Setup JDK
+        uses: actions/setup-java@v1
+        with:
+          java-version: 11
+      - name: Build
+        run: ./gradlew build packageUberJarForCurrentOS
diff --git a/.github/workflows/commit-release.yml b/.github/workflows/commit-release.yml
index cc9cfad..98ce813 100644
--- a/.github/workflows/commit-release.yml
+++ b/.github/workflows/commit-release.yml
@@ -1,4 +1,7 @@
-on: [push]
+on:
+  push:
+    branches:
+      - master
 
 name: Commit release
 
@@ -16,9 +19,13 @@ jobs:
       - name: Set version from gradle.properties
         run: echo "version=` cat gradle.properties | grep ^version= | grep -o [\\.0-9]* `" >> $GITHUB_ENV
       - name: Build
-        run: ./gradlew packageUberJarForCurrentOS
-        env:
-          additional_version: ""
+        run: ./gradlew build packageUberJarForCurrentOS
+      - name: Publish Web
+        uses: peaceiris/actions-gh-pages@v3
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          publish_dir: ./web/build/distributions
+          publish_branch: site
       - name: Create Release
         id: create_release
         uses: actions/create-release@v1
diff --git a/extensions.gradle b/extensions.gradle
index 0c108ed..2e89947 100644
--- a/extensions.gradle
+++ b/extensions.gradle
@@ -15,6 +15,7 @@ allprojects {
 
         mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
         mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
+        mppJsProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsProject.gradle"
         mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
 
         defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
diff --git a/mppAndroidProject.gradle b/mppAndroidProject.gradle
index 20b6516..97ddb3c 100644
--- a/mppAndroidProject.gradle
+++ b/mppAndroidProject.gradle
@@ -1,4 +1,4 @@
-project.version = "$version" + System.getenv("additional_version")
+project.version = "$version" + (System.getenv("additional_version") != null ? System.getenv("additional_version") : "")
 project.group = "$group"
 
 // apply from: "$publishGradlePath"
diff --git a/mppJavaProject.gradle b/mppJavaProject.gradle
index 2f64097..e2d5a08 100644
--- a/mppJavaProject.gradle
+++ b/mppJavaProject.gradle
@@ -1,4 +1,4 @@
-project.version = "$version" + System.getenv("additional_version")
+project.version = "$version" + (System.getenv("additional_version") != null ? System.getenv("additional_version") : "")
 project.group = "$group"
 
 // apply from: "$publishGradlePath"
diff --git a/mppJsProject.gradle b/mppJsProject.gradle
new file mode 100644
index 0000000..6ee908a
--- /dev/null
+++ b/mppJsProject.gradle
@@ -0,0 +1,34 @@
+project.version = "$version" + (System.getenv("additional_version") != null ? System.getenv("additional_version") : "")
+project.group = "$group"
+
+// apply from: "$publishGradlePath"
+
+kotlin {
+    js (IR) {
+        browser {
+            binaries.executable()
+        }
+        nodejs()
+    }
+
+    sourceSets {
+        commonMain {
+            dependencies {
+                implementation kotlin('stdlib')
+                api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
+            }
+        }
+        commonTest {
+            dependencies {
+                implementation kotlin('test-common')
+                implementation kotlin('test-annotations-common')
+            }
+        }
+        jsTest {
+            dependencies {
+                implementation kotlin('test-js')
+                implementation kotlin('test-junit')
+            }
+        }
+    }
+}
diff --git a/settings.gradle b/settings.gradle
index b1eed1e..29bbdc6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -9,7 +9,8 @@ rootProject.name = 'kmppscriptbuilder'
 
 String[] includes = [
         ":core",
-        ":desktop"
+        ":desktop",
+        ":web"
 ]
 
 
diff --git a/web/build.gradle b/web/build.gradle
new file mode 100644
index 0000000..7a3b6b1
--- /dev/null
+++ b/web/build.gradle
@@ -0,0 +1,17 @@
+plugins {
+    id "org.jetbrains.kotlin.multiplatform"
+    id "org.jetbrains.kotlin.plugin.serialization"
+}
+
+apply from: "$mppJsProjectPresetPath"
+
+kotlin {
+    sourceSets {
+        commonMain {
+            dependencies {
+                implementation project(":kmppscriptbuilder.core")
+                implementation "dev.inmo:micro_utils.common:$micro_utils_version"
+            }
+        }
+    }
+}
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/main.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/main.kt
new file mode 100644
index 0000000..36e52f8
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/main.kt
@@ -0,0 +1,83 @@
+package dev.inmo.kmppscriptbuilder.web
+
+import dev.inmo.kmppscriptbuilder.core.models.Config
+import dev.inmo.kmppscriptbuilder.core.utils.serialFormat
+import dev.inmo.kmppscriptbuilder.web.views.*
+import kotlinx.browser.document
+import kotlinx.dom.appendElement
+import org.w3c.dom.*
+import org.w3c.dom.url.URL
+import org.w3c.files.*
+
+fun saveFile(content: String, filename: String) {
+    val a = document.body!!.appendElement("a") {
+        setAttribute("style", "visibility:hidden; display: none")
+    } as HTMLAnchorElement
+    val blob = Blob(arrayOf(content), BlobPropertyBag(
+        "text/plain;charset=utf-8"
+    ))
+    val url = URL.createObjectURL(blob)
+    a.href = url
+    a.download = filename
+    a.target = "_blank"
+    a.click()
+    URL.revokeObjectURL(url)
+    a.remove()
+}
+
+fun main() {
+    document.addEventListener(
+        "DOMContentLoaded",
+        {
+            val builderView = BuilderView()
+
+            (document.getElementById("openConfig") as HTMLElement).onclick = {
+                val targetInput = document.body!!.appendElement("input") {
+                    setAttribute("style", "visibility:hidden; display: none")
+                } as HTMLInputElement
+                targetInput.type = "file"
+                targetInput.onchange = {
+                    targetInput.files ?.also { files ->
+                        for (i in (0 until files.length) ) {
+                            files[i] ?.also { file ->
+                                val reader = FileReader()
+
+                                reader.onload = {
+                                    val content = it.target.asDynamic().result as String
+                                    builderView.config = serialFormat.decodeFromString(Config.serializer(), content)
+                                    false
+                                }
+
+                                reader.readAsText(file)
+                            }
+                        }
+                    }
+                }
+                targetInput.click()
+                targetInput.remove()
+                false
+            }
+            (document.getElementById("saveConfig") as HTMLElement).onclick = {
+                val filename = "publish.kpsb"
+                val content = serialFormat.encodeToString(Config.serializer(), builderView.config)
+
+                saveFile(content, filename)
+
+                false
+            }
+            (document.getElementById("exportScript") as HTMLElement).onclick = {
+                val filename = "publish.gradle"
+
+                val content = builderView.config.run {
+                    type.buildMavenGradleConfig(
+                        mavenConfig,
+                        licenses
+                    )
+                }
+
+                saveFile(content, filename)
+                false
+            }
+        }
+    )
+}
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/utils/UkActive.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/utils/UkActive.kt
new file mode 100644
index 0000000..5871e05
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/utils/UkActive.kt
@@ -0,0 +1,13 @@
+package dev.inmo.kmppscriptbuilder.web.utils
+
+import org.w3c.dom.HTMLElement
+
+var HTMLElement.ukActive: Boolean
+    get() = classList.contains("uk-active")
+    set(value) {
+        if (value) {
+            classList.add("uk-active")
+        } else {
+            classList.remove("uk-active")
+        }
+    }
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/utils/keepScrolling.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/utils/keepScrolling.kt
new file mode 100644
index 0000000..00d7b0e
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/utils/keepScrolling.kt
@@ -0,0 +1,10 @@
+package dev.inmo.kmppscriptbuilder.web.utils
+
+import kotlinx.browser.document
+
+inline fun <R> keepScrolling(crossinline block: () -> R): R = document.body ?.let {
+    val (x, y) = (it.scrollLeft to it.scrollTop)
+    return block().also { _ ->
+        it.scrollTo(x, y)
+    }
+} ?: block()
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/BuilderView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/BuilderView.kt
new file mode 100644
index 0000000..ee06ba5
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/BuilderView.kt
@@ -0,0 +1,23 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.kmppscriptbuilder.core.models.Config
+import kotlinx.browser.document
+import org.w3c.dom.HTMLElement
+
+class BuilderView : View {
+    private val projectTypeView = ProjectTypeView()
+    private val licensesView = LicensesView(document.getElementById("licensesListDiv") as HTMLElement)
+    private val mavenInfoTypeView = MavenProjectInfoView()
+
+    var config: Config
+        get() = Config(
+            licensesView.licenses,
+            mavenInfoTypeView.mavenConfig,
+            projectTypeView.projectType
+        )
+        set(value) {
+            licensesView.licenses = value.licenses
+            mavenInfoTypeView.mavenConfig = value.mavenConfig
+            projectTypeView.projectType = value.type
+        }
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/DevelopersView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/DevelopersView.kt
new file mode 100644
index 0000000..2fdc7bd
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/DevelopersView.kt
@@ -0,0 +1,35 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.kmppscriptbuilder.core.models.Developer
+import org.w3c.dom.*
+
+class DevelopersView(rootElement: HTMLElement) : MutableListView<Developer>(rootElement, "Add developer", "Remove developer") {
+    private val HTMLElement.usernameElement: HTMLInputElement
+        get() = getElementsByTagName("input")[0] as HTMLInputElement
+    private val HTMLElement.nameElement: HTMLInputElement
+        get() = getElementsByTagName("input")[1] as HTMLInputElement
+    private val HTMLElement.emailElement: HTMLInputElement
+        get() = getElementsByTagName("input")[2] as HTMLInputElement
+
+    var developers: List<Developer>
+        get() = elements.map {
+            Developer(it.usernameElement.value, it.nameElement.value, it.emailElement.value)
+        }
+        set(value) {
+            data = value
+        }
+
+    override fun createPlainObject(): Developer = Developer("", "", "")
+
+    override fun HTMLElement.addContentBeforeRemoveButton(value: Developer) {
+        createTextField("Developer ID", "Developer username").value = value.id
+        createTextField("Developer name", "").value = value.name
+        createTextField("Developer E-Mail", "").value = value.eMail
+    }
+
+    override fun HTMLElement.updateElement(from: Developer, to: Developer) {
+        usernameElement.value = to.id
+        nameElement.value = to.name
+        emailElement.value = to.eMail
+    }
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/LicensesView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/LicensesView.kt
new file mode 100644
index 0000000..9588e26
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/LicensesView.kt
@@ -0,0 +1,113 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.kmppscriptbuilder.core.models.License
+import dev.inmo.kmppscriptbuilder.core.models.getLicenses
+import dev.inmo.micro_utils.coroutines.safeActor
+import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.flow.consumeAsFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.dom.appendElement
+import org.w3c.dom.*
+
+class LicensesView(
+    rootElement: HTMLElement,
+    client: HttpClient = HttpClient(),
+    scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
+) : MutableListView<License>(rootElement, "Add empty license", "Remove license") {
+    private val HTMLElement.idElement: HTMLInputElement
+        get() = getElementsByTagName("input")[0] as HTMLInputElement
+    private val HTMLElement.titleElement: HTMLInputElement
+        get() = getElementsByTagName("input")[1] as HTMLInputElement
+    private val HTMLElement.urlElement: HTMLInputElement
+        get() = getElementsByTagName("input")[2] as HTMLInputElement
+
+    private class LicenseOfferList(
+        rootElement: HTMLElement,
+        private val licensesView: LicensesView,
+        client: HttpClient,
+        scope: CoroutineScope
+    ) : ListView<License>(rootElement, useSimpleDiffStrategy = true) {
+        private var licensesTemplates: List<License> = emptyList()
+
+        init {
+            scope.launch {
+                licensesTemplates = client.getLicenses().values.toList()
+                changeActor.send(Unit) // update list of searches
+            }
+        }
+
+        private val changeActor: SendChannel<Unit> = scope.run {
+            val onChangeActor = Channel<Unit>(Channel.CONFLATED)
+            onChangeActor.consumeAsFlow().subscribeSafelyWithoutExceptions(scope) {
+                val lowercased = searchString
+                data = if (lowercased.isEmpty()) {
+                    emptyList()
+                } else {
+                    licensesTemplates.filter {
+                        val lowercasedTitle = it.title.toLowerCase()
+                        lowercased.all { it in lowercasedTitle }
+                    }
+                }
+            }
+            onChangeActor
+        }
+        private val searchElement = rootElement.createTextField("Quick add", "Type some license name part to find it").apply {
+            oninput = {
+                changeActor.offer(Unit)
+                false
+            }
+        }
+        private var searchString: String
+            get() = searchElement.value.toLowerCase()
+            set(value) {
+                searchElement.value = value
+            }
+
+        override fun HTMLElement.placeElement(value: License) {
+            createCommonButton(value.title).onclick = {
+                searchString = ""
+                licensesView.licenses += value
+                changeActor.offer(Unit)
+                false
+            }
+        }
+
+        override fun HTMLElement.updateElement(from: License, to: License) {
+            getElementsByTagName("button")[0] ?.remove()
+            placeElement(to)
+        }
+    }
+
+    private val licensesOffersList = LicenseOfferList(
+        rootElement.appendElement("div") { classList.add("uk-padding-small") } as HTMLElement,
+        this,
+        client,
+        scope
+    )
+
+    var licenses: List<License>
+        get() = elements.map {
+            License(it.idElement.value, it.titleElement.value, it.urlElement.value)
+        }
+        set(value) {
+            data = value
+        }
+
+    override fun createPlainObject(): License = License("", "", "")
+
+    override fun HTMLElement.addContentBeforeRemoveButton(value: License) {
+        createTextField("License Id", "Short name like \"Apache-2.0\"").value = value.id
+        createTextField("License Title", "Official title of license (like \"Apache Software License 2.0\")").value = value.title
+        createTextField("License URL", "Link to your LICENSE file OR official license file (like \"https://opensource.org/licenses/Apache-2.0\")").value = value.url ?: ""
+    }
+
+    override fun HTMLElement.updateElement(from: License, to: License) {
+        idElement.value = to.id
+        titleElement.value = to.title
+        urlElement.value = to.url ?: ""
+    }
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ListView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ListView.kt
new file mode 100644
index 0000000..700b325
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ListView.kt
@@ -0,0 +1,58 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.micro_utils.common.calculateStrictDiff
+import kotlinx.dom.appendElement
+import org.w3c.dom.HTMLElement
+
+abstract class ListView<T>(
+    protected val rootElement: HTMLElement,
+    useSimpleDiffStrategy: Boolean = false
+) : View {
+    protected val elements = mutableListOf<HTMLElement>()
+    private val diffHandling: (old: List<T>, new: List<T>) -> Unit = if (useSimpleDiffStrategy) {
+        { _, new ->
+            elements.forEach { it.remove() }
+            elements.clear()
+            new.forEach {
+                val element = instantiateElement()
+                elements.add(element)
+                element.placeElement(it)
+            }
+        }
+    } else {
+        {  old, new ->
+            val diff = old.calculateStrictDiff(new)
+            diff.removed.forEach {
+                elements[it.index].remove()
+                elements.removeAt(it.index)
+                println(it.value)
+            }
+            diff.added.forEach {
+                val element = instantiateElement()
+                elements.add(element)
+                element.placeElement(it.value)
+            }
+            diff.replaced.forEach { (old, new) ->
+                val element = elements.getOrNull(old.index) ?.also { it.updateElement(old.value, new.value) }
+                if (element == null) {
+                    val newElement = instantiateElement()
+                    newElement.placeElement(new.value)
+                    elements[new.index] = newElement
+                }
+            }
+        }
+    }
+    protected var data: List<T> = emptyList()
+        set(value) {
+            val old = field
+            field = value
+            diffHandling(old, value)
+        }
+
+    protected abstract fun HTMLElement.placeElement(value: T)
+    protected abstract fun HTMLElement.updateElement(from: T, to: T)
+
+    private fun instantiateElement() = rootElement.appendElement("div") {
+        classList.add("uk-padding-small")
+    } as HTMLElement
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/MavenProjectInfoView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/MavenProjectInfoView.kt
new file mode 100644
index 0000000..b9cbddc
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/MavenProjectInfoView.kt
@@ -0,0 +1,44 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.kmppscriptbuilder.core.models.MavenConfig
+import dev.inmo.kmppscriptbuilder.core.models.SonatypeRepository
+import kotlinx.browser.document
+import org.w3c.dom.HTMLElement
+import org.w3c.dom.HTMLInputElement
+
+class MavenProjectInfoView : View {
+    private val nameElement = document.getElementById("projectNameInput") as HTMLInputElement
+    private val descriptionElement = document.getElementById("projectDescriptionInput") as HTMLInputElement
+    private val urlElement = document.getElementById("projectUrlInput") as HTMLInputElement
+    private val vcsUrlElement = document.getElementById("projectVCSUrlInput") as HTMLInputElement
+    private val includeGpgElement = document.getElementById("includeGpgSignToggle") as HTMLInputElement
+    private val includeMavenCentralElement = document.getElementById("includeMavenCentralTargetRepoToggle") as HTMLInputElement
+    private val developersView = DevelopersView(document.getElementById("developersListDiv") as HTMLElement)
+    private val repositoriesView = RepositoriesView(document.getElementById("repositoriesListDiv") as HTMLElement)
+
+    var mavenConfig: MavenConfig
+        get() = MavenConfig(
+            nameElement.value,
+            descriptionElement.value,
+            urlElement.value,
+            vcsUrlElement.value,
+            includeGpgElement.checked,
+            developersView.developers,
+            repositoriesView.repositories + if (includeMavenCentralElement.checked) {
+                listOf(SonatypeRepository)
+            } else {
+                emptyList()
+            }
+        )
+        set(value) {
+            nameElement.value = value.name
+            descriptionElement.value = value.description
+            urlElement.value = value.url
+            vcsUrlElement.value = value.vcsUrl
+            includeGpgElement.checked = value.includeGpgSigning
+            developersView.developers = value.developers
+            val reposWithoutSonatype = value.repositories.filter { it != SonatypeRepository }
+            includeMavenCentralElement.checked = value.repositories.size != reposWithoutSonatype.size
+            repositoriesView.repositories = value.repositories
+        }
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/MutableListView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/MutableListView.kt
new file mode 100644
index 0000000..05d7ee9
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/MutableListView.kt
@@ -0,0 +1,41 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.kmppscriptbuilder.web.utils.keepScrolling
+import org.w3c.dom.HTMLElement
+
+abstract class MutableListView<T>(
+    rootElement: HTMLElement,
+    addButtonText: String = "Add",
+    private val removeButtonText: String = "Remove"
+) : ListView<T>(rootElement) {
+    init {
+        rootElement.createPrimaryButton(addButtonText).apply {
+            onclick = {
+                keepScrolling {
+                    val newObject = createPlainObject()
+                    data += newObject
+                }
+                false
+            }
+        }
+    }
+
+    protected abstract fun createPlainObject(): T
+    protected open fun HTMLElement.addContentBeforeRemoveButton(value: T) {}
+    protected open fun HTMLElement.addContentAfterRemoveButton(value: T) {}
+    final override fun HTMLElement.placeElement(value: T) {
+        addContentBeforeRemoveButton(value)
+        addRemoveButton()
+        addContentAfterRemoveButton(value)
+    }
+
+    private fun HTMLElement.addRemoveButton() {
+        val button = createPrimaryButton(removeButtonText)
+        button.onclick = {
+            elements.indexOf(button.parentElement).takeIf { it > -1 } ?.also {
+                data -= data[it]
+            } ?: rootElement.removeChild(this@addRemoveButton)
+            false
+        }
+    }
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ProjectTypeView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ProjectTypeView.kt
new file mode 100644
index 0000000..accaaa7
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ProjectTypeView.kt
@@ -0,0 +1,33 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.kmppscriptbuilder.core.models.*
+import dev.inmo.kmppscriptbuilder.web.utils.ukActive
+import kotlinx.browser.document
+import org.w3c.dom.HTMLElement
+
+class ProjectTypeView : View {
+    private val mppProjectTypeElement = document.getElementById("mppProjectType") as HTMLElement
+    private val jvmProjectTypeElement = document.getElementById("jvmProjectType") as HTMLElement
+
+    var projectType: ProjectType
+        get() = if (jvmProjectTypeElement.ukActive) {
+            JVMProjectType
+        } else {
+            MultiplatformProjectType
+        }
+        set(value) {
+            mppProjectTypeElement.ukActive = value == MultiplatformProjectType
+            jvmProjectTypeElement.ukActive = value == JVMProjectType
+        }
+
+    init {
+        mppProjectTypeElement.onclick = {
+            projectType = MultiplatformProjectType
+            Unit
+        }
+        jvmProjectTypeElement.onclick = {
+            projectType = JVMProjectType
+            Unit
+        }
+    }
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/RepositoriesView.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/RepositoriesView.kt
new file mode 100644
index 0000000..93234ca
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/RepositoriesView.kt
@@ -0,0 +1,31 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository
+import org.w3c.dom.*
+
+class RepositoriesView(rootElement: HTMLElement) : MutableListView<MavenPublishingRepository>(rootElement, "Add repository", "Remove repository") {
+    private val HTMLElement.nameElement: HTMLInputElement
+        get() = getElementsByTagName("input")[0] as HTMLInputElement
+    private val HTMLElement.urlElement: HTMLInputElement
+        get() = getElementsByTagName("input")[1] as HTMLInputElement
+
+    var repositories: List<MavenPublishingRepository>
+        get() = elements.map {
+            MavenPublishingRepository(it.nameElement.value, it.urlElement.value)
+        }
+        set(value) {
+            data = value
+        }
+
+    override fun createPlainObject(): MavenPublishingRepository = MavenPublishingRepository("", "")
+
+    override fun HTMLElement.addContentBeforeRemoveButton(value: MavenPublishingRepository) {
+        createTextField("Repository name", "This name will be used to identify repository in grade").value = value.name
+        createTextField("Repository URL", "For example: https://repo.maven.apache.org/maven2/").value = value.name
+    }
+
+    override fun HTMLElement.updateElement(from: MavenPublishingRepository, to: MavenPublishingRepository) {
+        nameElement.value = to.name
+        urlElement.value = to.url
+    }
+}
\ No newline at end of file
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/View.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/View.kt
new file mode 100644
index 0000000..cf07625
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/View.kt
@@ -0,0 +1,3 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+interface View
diff --git a/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ViewElements.kt b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ViewElements.kt
new file mode 100644
index 0000000..7f70fdc
--- /dev/null
+++ b/web/src/jsMain/kotlin/dev/inmo/kmppscriptbuilder/web/views/ViewElements.kt
@@ -0,0 +1,35 @@
+package dev.inmo.kmppscriptbuilder.web.views
+
+import kotlinx.dom.appendElement
+import org.w3c.dom.*
+
+fun HTMLElement.createTextField(
+    label: String,
+    placeholder: String
+): HTMLInputElement {
+    return appendElement("div") {
+        classList.add("uk-margin", "uk-width-1-1")
+    }.appendElement("label") {
+        classList.add("uk-form-label")
+        innerHTML = label
+    }.run {
+        val input = appendElement("input") {
+            classList.add("uk-input", "uk-width-expand")
+            setAttribute("type", "text")
+            setAttribute("placeholder", placeholder)
+        } as HTMLInputElement
+        input
+    }
+}
+
+fun HTMLElement.createPrimaryButton(text: String): HTMLButtonElement = (appendElement("button") {
+    classList.add("uk-button", "uk-button-primary")
+} as HTMLButtonElement).apply {
+    innerText = text
+}
+
+fun HTMLElement.createCommonButton(text: String): HTMLButtonElement = (appendElement("button") {
+    classList.add("uk-button", "uk-button-default")
+} as HTMLButtonElement).apply {
+    innerText = text
+}
diff --git a/web/src/jsMain/resources/index.html b/web/src/jsMain/resources/index.html
new file mode 100644
index 0000000..4f3a5ea
--- /dev/null
+++ b/web/src/jsMain/resources/index.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Kotlin Publication Scripts Builder</title>
+    <!-- UIkit CSS -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.6.17/dist/css/uikit.min.css" />
+</head>
+<body>
+    <nav class="uk-navbar-container" uk-navbar>
+        <div class="uk-navbar-left">
+            <div class="uk-padding-small uk-text-lead">Kotlin Publication Scripts Builder</div>
+        </div>
+        <div class="uk-navbar-right">
+            <ul class="uk-navbar-nav">
+                <li uk-tooltip="title: Open config" id="openConfig"><a href="#"><span uk-icon="icon: pull"></span></a></li><!--Open file-->
+                <li uk-tooltip="title: Save config" id="saveConfig"><a href="#"><span uk-icon="icon: push"></span></a></li><!--Save file-->
+                <li uk-tooltip="title: Export script" id="exportScript"><a href="#"><span uk-icon="icon: upload"></span></a></li><!--Save file-->
+            </ul>
+        </div>
+    </nav>
+    <form class="uk-padding-small">
+        <fieldset class="uk-fieldset">
+            <legend class="uk-legend">Project type</legend>
+            <div class="uk-padding-small">
+                <ul class="uk-subnav uk-subnav-pill">
+                    <li id="mppProjectType" class="uk-active"><a href="#">Multiplatform</a></li>
+                    <li id="jvmProjectType"><a href="#">JVM</a></li>
+                </ul>
+            </div>
+            <legend class="uk-legend">Licenses</legend>
+            <div id="licensesListDiv" class="uk-padding-small">
+<!--                <div class="uk-margin uk-width-1-1">-->
+<!--                    <input id="searchFilterInput" class="uk-input uk-width-expand" type="text" placeholder="License search filter">-->
+<!--                </div>-->
+<!--                <button class="uk-button uk-button-primary">Add empty license</button>-->
+            </div>
+
+            <legend class="uk-legend">Project information</legend>
+
+            <div class="uk-padding-small">
+                <div class="uk-margin uk-width-1-1">
+                    <label class="uk-form-label" for="projectNameInput">Public project name</label>
+                    <input id="projectNameInput" class="uk-input uk-width-expand" type="text" placeholder="${project.name}">
+                </div>
+                <div class="uk-margin uk-width-1-1">
+                    <label class="uk-form-label" for="projectDescriptionInput">Public project description</label>
+                    <input id="projectDescriptionInput" class="uk-input uk-width-expand" type="text" placeholder="${project.name}">
+                </div>
+                <div class="uk-margin uk-width-1-1">
+                    <label class="uk-form-label" for="projectUrlInput">Public project URL</label>
+                    <input id="projectUrlInput" class="uk-input uk-width-expand" type="text" placeholder="Type url to github or other source with readme">
+                </div>
+                <div class="uk-margin uk-width-1-1">
+                    <label class="uk-form-label" for="projectVCSUrlInput">Public project VCS URL (with .git)</label>
+                    <input id="projectVCSUrlInput" class="uk-input uk-width-expand" type="text" placeholder="Type url to github .git file">
+                </div>
+
+                <div class="uk-margin">
+                    <label><input id="includeGpgSignToggle" class="uk-checkbox" type="checkbox" checked> Include GPG Signing</label>
+                </div>
+                <div class="uk-margin">
+                    <label><input id="includeMavenCentralTargetRepoToggle" class="uk-checkbox" type="checkbox"> Include publication to MavenCentral</label>
+                </div>
+            </div>
+
+            <legend class="uk-legend">Developers info</legend>
+            <div id="developersListDiv" class="uk-padding-small"></div>
+
+            <legend class="uk-legend">Repositories info</legend>
+            <div id="repositoriesListDiv" class="uk-padding-small"></div>
+        </fieldset>
+    </form>
+    <!-- UIkit JS -->
+    <script src="https://cdn.jsdelivr.net/npm/uikit@3.6.17/dist/js/uikit.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/uikit@3.6.17/dist/js/uikit-icons.min.js"></script>
+    <!-- Internal JS -->
+    <script src="kmppscriptbuilder.web.js"></script>
+</body>
+</html>
\ No newline at end of file