
React-like declarative UI with typesafe builders and virtual DOM; modular VMVC, SPA router and CSS-in-code; annotation-driven RMI codegen enabling stateless, allowlisted client-server RPC and scaffolds.
Visage is a WEB frontend framework for Kotlin. The main goal of this project is to create a framework which is designed from the ground up to work perfectly with Kotlin and utilise the language features to make frontend developers life much easier covering most of the aspects of frontend development.
The documentation can be found on the webpage: https://visageui.appspot.com
At this time it's in a very early stage and under active development. Stay tuned to see whats coming.
Visage contains different modules not just the UI Part. At this time, Visage is an all in one library which means all the following modules are packed in the Visage library, but you don't need to use all of them. In a later time it may be sliced to different standalone modules but not now.
The core of Visage is a React like UI API which enables you to create stunning frontends a declarative way using Kotlin's typesafe builders. Just like in React you can put together default HTML elements but you can also define your own components to split your code. Kotlin as a language has much more potential than which can be achieved with simple JSX or with a React wrapper. The goal of this module is to keep what is outstanding in React (declarative UI development and virtual DOM) but enhance it and fix some problems with React which can be done better using Kotlin.
VMVC is consist to (ViewModel-View-Controller) a mixed and modified version of MVC, MVP and MVVM. This module can be used to separate the view from the data which is rendered and the business logic between the two. Nothing really special, just some base classes and best practices which makes your code cleaner. This is an optional module. You don't need to use this module or follow the guidelines. You can use your own structure if you want.
The router module makes it easy to define what (Component / Scene) needs to be rendered on specific URL-s. Useful for Single Page Applications. Similar to React Router. This is an optional module. You don't need to use it, you can use your own navigation / routing code if you want.
This module is responsible to easily create CSS classes in Kotlin code, next to your components, or wherever you want. Similar to Typestyle for React/Typescript. This is an optional module, you don't need to use it. You can define your CSS howewer you want.
This module helps a lot to communicate with any JVM backend. The goal of this project is to create client-server communication as painful as possible, using annotation processor generated shared controllers, which methods can be called from the client as a single method call. You will learn more from the concept of sharing controller and model between frontend and backend as a stateless way but it is similar what JSF do but in a stateless and typesafe way. This is also an optional module, you don't need to use it, you can use your own method to communicate with any backend using fetch or whatever.
A simple but powerful common validation library to eliminate the need to rewrite the validation code on both client and server side. This is also an optional modul.
This module contains basic Visage components (like Button, TextField, RadioButton, etc...) and layouts (like HBox, VBox, Grid, etc...) which can be used out of the box to create stunning UI-s. This is an optional module, you don't need to use any of this components, you can use your own or any other third party Visage components.
Visage is published to Maven Central under the io.pxlworks namespace and is built
with Kotlin Multiplatform (JVM + JS), Kotlin 2.4, Gradle 9 and JDK 21.
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.google.devtools.ksp") // only needed if you use the RMI module
}
kotlin {
jvm()
js { browser() }
sourceSets {
commonMain.dependencies {
implementation("io.pxlworks:visage:<version>")
}
}
}
// RMI code generation runs per target: the backend base on JVM, the frontend
// base on JS. Both read the shared @VisageController interface from commonMain.
dependencies {
add("kspJvm", "io.pxlworks:visage-processor:<version>")
add("kspJs", "io.pxlworks:visage-processor:<version>")
}Declare the contract once in commonMain:
@Serializable data class GreeterProps(val title: String)
@Serializable data class GreeterModel(val greeting: String)
@VisageController(props = GreeterProps::class, model = GreeterModel::class)
interface IGreeter {
fun greet(name: String)
@Authenticated(minLevel = 5) // gate is generated into the server registry
fun secret(topic: String)
}The KSP processor generates the always-regenerated base classes into
build/generated/ksp/...: AGreeterControllerBe (JVM) and
AGreeterControllerFe + GreeterControllerBeProxy (JS).
The processor also generates a JVM-only GeneratedVisageControllers registry:
a Map<String, VisageActionRunner> keyed by the fully-qualified action
(sample.GreeterControllerBe.greet). Each runner instantiates the concrete
controller and invokes the action with plain typed calls — no reflection and no
client-controlled class loading. VisageRmiHandler.callRmi(req, resp, userLevelProvider, GeneratedVisageControllers.runners) is fail-closed: a
request whose action key is not in the registry gets 403 without touching any
controller. @Authenticated checks (declared on the interface method) are baked
into the matching runner and enforced against the level from your
IVisageRmiUserLevelProvider before the controller is constructed.
You then write the concrete classes by hand, once. You can generate the initial skeletons with the Visage Gradle plugin instead of typing them:
plugins {
id("io.pxlworks.visage.scaffold") version "<version>"
}./gradlew generateVisageScaffoldsThis reads the KSP descriptor and writes GreeterControllerBe.kt (jvmMain),
GreeterScene.kt and GreeterControllerFe.kt (jsMain) into src/ — but only
if they don't already exist, so it never overwrites your edits. (KSP itself
cannot do this; it only writes to its managed output dir, hence the separate
plugin task.) After the first generation the files are yours to fill in.
Backend (jvmMain):
class GreeterControllerBe : AGreeterControllerBe() {
override fun greet(name: String): GreeterModel = GreeterModel("Hello, $name!")
}Frontend (jsMain):
class GreeterSceneState
class MGreeterScene(props: GreeterProps) :
AScene<GreeterProps, GreeterModel, GreeterSceneState, GreeterControllerFe>(props) {
override fun createController() = GreeterControllerFe(props, this)
override fun initState() = GreeterSceneState()
override fun Components.render(children: List<AComponent<*>>) {
+this@MGreeterScene.model.greeting
}
}
class GreeterControllerFe(props: GreeterProps, scene: MGreeterScene) :
AGreeterControllerFe(props, scene) {
override fun createInitialModel() = GreeterModel(props.title)
override fun init() { /* e.g. call_greet("world") */ }
}A complete, buildable example lives in the sample module.
The build lives under src/visage (a multi-module Gradle build):
:visage — the Kotlin Multiplatform framework:visage-processor — the KSP symbol processor for the RMI module:visage-gradle-plugin — the generateVisageScaffolds Gradle plugin:sample — a small module that exercises the processor end-to-endcd src/visage
./gradlew build # requires JDK 21The sample module is also a runnable end-to-end demo: a JDK HttpServer
serves the compiled JS app and a /visage/_rmi endpoint wired to the backend
controller, so the full client-server round-trip runs in the browser.
cd src/visage
./gradlew :sample:runServer # then open http://localhost:8087The page first shows the initial model, then updates to Hello, world! once the
frontend's greet() call reaches the backend GreeterControllerBe and returns.
Note: the
src/visage-docdemo application has not yet been migrated and still targets the old Kotlin 1.4 / kapt toolchain.
Visage is a WEB frontend framework for Kotlin. The main goal of this project is to create a framework which is designed from the ground up to work perfectly with Kotlin and utilise the language features to make frontend developers life much easier covering most of the aspects of frontend development.
The documentation can be found on the webpage: https://visageui.appspot.com
At this time it's in a very early stage and under active development. Stay tuned to see whats coming.
Visage contains different modules not just the UI Part. At this time, Visage is an all in one library which means all the following modules are packed in the Visage library, but you don't need to use all of them. In a later time it may be sliced to different standalone modules but not now.
The core of Visage is a React like UI API which enables you to create stunning frontends a declarative way using Kotlin's typesafe builders. Just like in React you can put together default HTML elements but you can also define your own components to split your code. Kotlin as a language has much more potential than which can be achieved with simple JSX or with a React wrapper. The goal of this module is to keep what is outstanding in React (declarative UI development and virtual DOM) but enhance it and fix some problems with React which can be done better using Kotlin.
VMVC is consist to (ViewModel-View-Controller) a mixed and modified version of MVC, MVP and MVVM. This module can be used to separate the view from the data which is rendered and the business logic between the two. Nothing really special, just some base classes and best practices which makes your code cleaner. This is an optional module. You don't need to use this module or follow the guidelines. You can use your own structure if you want.
The router module makes it easy to define what (Component / Scene) needs to be rendered on specific URL-s. Useful for Single Page Applications. Similar to React Router. This is an optional module. You don't need to use it, you can use your own navigation / routing code if you want.
This module is responsible to easily create CSS classes in Kotlin code, next to your components, or wherever you want. Similar to Typestyle for React/Typescript. This is an optional module, you don't need to use it. You can define your CSS howewer you want.
This module helps a lot to communicate with any JVM backend. The goal of this project is to create client-server communication as painful as possible, using annotation processor generated shared controllers, which methods can be called from the client as a single method call. You will learn more from the concept of sharing controller and model between frontend and backend as a stateless way but it is similar what JSF do but in a stateless and typesafe way. This is also an optional module, you don't need to use it, you can use your own method to communicate with any backend using fetch or whatever.
A simple but powerful common validation library to eliminate the need to rewrite the validation code on both client and server side. This is also an optional modul.
This module contains basic Visage components (like Button, TextField, RadioButton, etc...) and layouts (like HBox, VBox, Grid, etc...) which can be used out of the box to create stunning UI-s. This is an optional module, you don't need to use any of this components, you can use your own or any other third party Visage components.
Visage is published to Maven Central under the io.pxlworks namespace and is built
with Kotlin Multiplatform (JVM + JS), Kotlin 2.4, Gradle 9 and JDK 21.
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.google.devtools.ksp") // only needed if you use the RMI module
}
kotlin {
jvm()
js { browser() }
sourceSets {
commonMain.dependencies {
implementation("io.pxlworks:visage:<version>")
}
}
}
// RMI code generation runs per target: the backend base on JVM, the frontend
// base on JS. Both read the shared @VisageController interface from commonMain.
dependencies {
add("kspJvm", "io.pxlworks:visage-processor:<version>")
add("kspJs", "io.pxlworks:visage-processor:<version>")
}Declare the contract once in commonMain:
@Serializable data class GreeterProps(val title: String)
@Serializable data class GreeterModel(val greeting: String)
@VisageController(props = GreeterProps::class, model = GreeterModel::class)
interface IGreeter {
fun greet(name: String)
@Authenticated(minLevel = 5) // gate is generated into the server registry
fun secret(topic: String)
}The KSP processor generates the always-regenerated base classes into
build/generated/ksp/...: AGreeterControllerBe (JVM) and
AGreeterControllerFe + GreeterControllerBeProxy (JS).
The processor also generates a JVM-only GeneratedVisageControllers registry:
a Map<String, VisageActionRunner> keyed by the fully-qualified action
(sample.GreeterControllerBe.greet). Each runner instantiates the concrete
controller and invokes the action with plain typed calls — no reflection and no
client-controlled class loading. VisageRmiHandler.callRmi(req, resp, userLevelProvider, GeneratedVisageControllers.runners) is fail-closed: a
request whose action key is not in the registry gets 403 without touching any
controller. @Authenticated checks (declared on the interface method) are baked
into the matching runner and enforced against the level from your
IVisageRmiUserLevelProvider before the controller is constructed.
You then write the concrete classes by hand, once. You can generate the initial skeletons with the Visage Gradle plugin instead of typing them:
plugins {
id("io.pxlworks.visage.scaffold") version "<version>"
}./gradlew generateVisageScaffoldsThis reads the KSP descriptor and writes GreeterControllerBe.kt (jvmMain),
GreeterScene.kt and GreeterControllerFe.kt (jsMain) into src/ — but only
if they don't already exist, so it never overwrites your edits. (KSP itself
cannot do this; it only writes to its managed output dir, hence the separate
plugin task.) After the first generation the files are yours to fill in.
Backend (jvmMain):
class GreeterControllerBe : AGreeterControllerBe() {
override fun greet(name: String): GreeterModel = GreeterModel("Hello, $name!")
}Frontend (jsMain):
class GreeterSceneState
class MGreeterScene(props: GreeterProps) :
AScene<GreeterProps, GreeterModel, GreeterSceneState, GreeterControllerFe>(props) {
override fun createController() = GreeterControllerFe(props, this)
override fun initState() = GreeterSceneState()
override fun Components.render(children: List<AComponent<*>>) {
+this@MGreeterScene.model.greeting
}
}
class GreeterControllerFe(props: GreeterProps, scene: MGreeterScene) :
AGreeterControllerFe(props, scene) {
override fun createInitialModel() = GreeterModel(props.title)
override fun init() { /* e.g. call_greet("world") */ }
}A complete, buildable example lives in the sample module.
The build lives under src/visage (a multi-module Gradle build):
:visage — the Kotlin Multiplatform framework:visage-processor — the KSP symbol processor for the RMI module:visage-gradle-plugin — the generateVisageScaffolds Gradle plugin:sample — a small module that exercises the processor end-to-endcd src/visage
./gradlew build # requires JDK 21The sample module is also a runnable end-to-end demo: a JDK HttpServer
serves the compiled JS app and a /visage/_rmi endpoint wired to the backend
controller, so the full client-server round-trip runs in the browser.
cd src/visage
./gradlew :sample:runServer # then open http://localhost:8087The page first shows the initial model, then updates to Hello, world! once the
frontend's greet() call reaches the backend GreeterControllerBe and returns.
Note: the
src/visage-docdemo application has not yet been migrated and still targets the old Kotlin 1.4 / kapt toolchain.