Kotlin
A concise multiplatform language developed by JetBrains
Exploring Compose HTML for Server Side Rendering
Something is happening in server-rendered web development. React shipped Server Components. HTMX made “hypermedia” cool again. Phoenix LiveView proved a server can push interactive UI updates without a client framework in sight. Every ecosystem seems to be rediscovering the server as a place to render UI, except one: the JVM. What if Compose, the UI toolkit already spanning Android, Desktop, and iOS, took a shot at server-rendering HTML too?
The vision is simple: give backend developers a way to build server-rendered UI as type-safe, reusable Compose components (real Kotlin, with autocomplete, refactoring, and compiler checks) instead of string-based templates. No separate templating language, no separate UI codebase to maintain alongside the backend. This blog serves to explore some ideas how to achieve this vision and represents an exploration instead of an official commitment.
Every major JS framework now has an SSR story: React has Next, Vue has Nuxt, Svelte has SvelteKit. And it’s not only the JS ecosystem. C#, Rust and even functional languages like Elixir have innovative solutions to build fullstack apps without relying on templating engines. Instead, they bundle state and rendering into reusable components, directly in code, the same way Compose already does everywhere else.
Right now the JVM doesn’t have a horse in this race. There’s no shortage of SSR libraries on the JVM. But most of them need some sort of templating language and have nothing close enough to a component for a JS dev to recognize as such.
But there is already a framework that is battle-tested and capable of filling this gap for the JVM, it just never really targeted the server. Compose Multiplatform allows us to write business logic and User Interfaces once and share it between platforms: Android, iOS, Desktop, and the web. It just needs to make the jump to the server next.
Compose Multiplatform already targets the web, but not the way you’d want for this: it renders directly into a canvas, which shares UI code between mobile platforms and the browser at the cost of SEO, loading times, and accessibility.
A way to render HTML with Compose already exists, and it’s older than Compose for Web: Compose HTML, which uses the Compose runtime to build SPAs in Kotlin and compile it to JS using the Kotlin/JS compiler. Add a JVM target and it could do SSR too. The rendering happens directly in Kotlin: real components, real types, no templating language.
JVM devs stuck with Thymeleaf/JSP, or reaching for a separate JS framework just to build fullstack applications, wouldn’t have to leave the platform: type-safe, reusable Compose components replace what the templating language used to handle. Kotlin’s Java interoperability means it would slot into large legacy Java applications too.
Take something as basic as a reusable card component. In Thymeleaf, that’s a fragment defined in its own file, called by name, with parameters passed as untyped strings:
<!-- fragments/card.html -->
<div th:fragment="card(title, count)" class="card">
<h3 th:text="${title}">Title</h3>
<span th:text="${count}">0</span>
</div>
<!-- usage -->
<div th:replace="~{fragments/card :: card(title='Cart', count=${cartCount})}"></div>
<div th:replace="~{fragments/card :: card(title='Wishlist', count=${wishlistCount})}"></div>
Rename count to itemCount and every call site keeps compiling until it breaks at runtime. The compiler has no idea card or its parameters even exist.
The same component in Compose is a typed function:
@Composable
fun Card(title: String, count: Int) {
Div({ classes("card") }) {
H3 { Text(title) }
Span { Text(count.toString()) }
}
}
// usage
Card(title = "Cart", count = cartCount)
Card(title = "Wishlist", count = wishlistCount)
Rename count here and every call site either updates with the IDE or fails to compile. Pass a String where an Int is expected, and it’s a compiler error, not a runtime surprise.
Today Compose HTML only has a JS target, so it can only be used from the browser; there’s no way of doing SSR yet. That doesn’t mean the Kotlin web-dev ecosystem is standing still, though.
There is Kobweb, a batteries-included framework built on top of Compose HTML. It doesn’t offer SSR but supports static site export/prerendering to help with SEO. There is also Kilua, which doesn’t build on top of Compose HTML but on top of the Compose Runtime directly to do SSR and CSR, leveraging JS or Wasm, and offers integrations for Ktor, Spring Boot, and others. And there is Summon, with SSR and hydration support.
There’s already a small but active community leveraging Compose to build for the web. Adding SSR capabilities to Compose HTML would give Kobweb, Kilua, and Summon a shared foundation instead of three separate approaches, and give frameworks like Spring Boot and Ktor a good reason to integrate with it on the server.
This space isn’t totally unexplored, but everything from this point onward is pure exploration.
What Compose HTML on the server could look like
The first step would be to add a JVM target to Compose HTML, which is a bit easier said than done. There would need to be renderToString and renderToBytes functions that run a composition once on the JVM and serialize the resulting tree into a string.
fun renderToString(content: @Composable DOMScope<DomElement>.() -> Unit): String
val html: String = renderToString {
Div({ classes("card") }) {
Text("Hello")
Span({ classes("title") }) {
Text("World")
}
}
}
// html == """<div class="card">Hello<span class="title">World</span></div>"""
It composes once, lets the initial composition settle, walks the resulting tree, and serializes it straight to an HTML string: no browser, no DOM.
There are some limitations to this. There would probably be only a single render pass, meaning no recomposition on state change or any effects, in essence very similar to SSR in JS. Event listeners should be accepted but will be inert; there’s no point in binding to browser events on the server.
This would probably already be enough to build basic, entirely server-rendered pages using Compose. Here’s a full todo app on Spring Boot:
@Controller
class TodoController(private val todoService: TodoService) {
@GetMapping("/todos")
@ResponseBody
fun todoView(): String = renderToString {
TodoView(todoService)
}
@PostMapping("/todos")
fun addTodo(createTodoDto: CreateTodoDto): String {
todoService.addTodo(createTodoDto.title)
return "redirect:/todos"
}
@PostMapping("/complete/{id}")
fun completeTodo(@PathVariable id: Long): String {
todoService.completeTodo(id)
return "redirect:/todos"
}
}
data class CreateTodoDto(val title: String)
@Composable
fun TodoView(todoService: TodoService) {
AddTodo()
TodoList(todoService)
}
@Composable
fun AddTodo() {
Form(
attrs = {
action("/todos")
method(FormMethod.Post)
}
) {
TextInput(
attrs = {
placeholder("Add todo")
name(CreateTodoDto::title.name)
}
)
Button(
attrs = {
type(ButtonType.Submit)
}
) {
Text("Add")
}
}
}
@Composable
fun TodoList(todoService: TodoService) {
val todos by produceState(initialValue = emptyList<Todo>(), todoService) {
value = todoService.getTodos()
}
Ul {
todos.forEach { todo ->
Li {
Form(
attrs = {
action("/complete/${todo.id}")
method(FormMethod.Post)
}
) {
Text(todo.title)
Button(
attrs = {
type(ButtonType.Submit)
}
) {
Text("Complete")
}
}
}
}
}
}
Every interaction here is a real HTTP form submission and full-page redirect: no client JS at all, same as classic Thymeleaf-style SSR, just written entirely in Compose.
At that point, frameworks like Spring and Ktor could start experimenting with integrations and identifying missing integration points. This would also be the first sensible point at which new libraries (e.g. components) could be created.
Going entirely off the rails into pure speculation, this is what such an integration could look like for Spring:
@ComposePage("/todos")
@Composable
fun TodosPage(todoService: TodoService) {
AddTodo()
TodoList(todoService)
}
@ComposeAction("/todos", method = PostMapping::class)
fun addTodo(
@RequestBody createTodoDto: CreateTodoDto,
todoService: TodoService
) {
todoService.addTodo(createTodoDto.title)
}
The idea: a hypothetical Spring integration could turn a @Composable function directly into a routed page, no manual renderToString call, no controller boilerplate, no wrapping HTML shell. Spring would own request mapping and dependency injection exactly like it does today; Compose HTML would just be the render target instead of a View/template.
Or for Ktor:
routing {
composable("/todos") {
TodoView(todoService)
}
post("/todos") {
val params = call.receiveParameters()
todoService.addTodo(params["title"]!!)
call.respondRedirect("/todos")
}
}
composable(path) { } would be a thin wrapper Ktor could add: call renderToString internally and respond with the HTML content type, so a route body becomes a @Composable lambda instead of a string template or manual call.respondText.
Worth repeating: these are illustrative sketches, not planned APIs, not a roadmap.
Hydration and state sync are the natural next question, not an answer: how would a composable that already rendered on the server pick up interactivity in the browser, and would client and server ever need to agree on state? Answering that would also open the door to sharing UI code between client and server, the same component compiled once for the browser and once for the server, and enable interactive fullstack web apps built entirely in Kotlin.
Let’s be clear about scope: the goal is not to expand Compose HTML into a fully-fledged, batteries-included framework. Rather, the vision is similar to React’s: stay small and let frameworks build the integration points on top, just applied to a multiplatform library instead of a single-platform one. Framework integrations and ecosystem libraries live outside the core. That’s a real contrast to the rest of Compose Multiplatform, which ships official libraries for Material3 components, state management, and many other things. Compose HTML will need to rely on the Kotlin community and ecosystem to figure out what integration points are actually needed and how its future will look, instead of dictating a direction from the inside.
We are already talking to framework maintainers from Kobweb, Kilua, and Summon to gather their perspective, as well as the Spring team, which has expressed interest in experimenting once a JVM target is added to Compose HTML.
If you want to talk shop, argue with any of this, or just see where it goes, join the Kotlinlang Slack (get your invite here: https://kotl.in/slack) and the #compose-ssr channel.
Every other ecosystem already took its shot at the server. Kotlin’s turn is overdue.