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:
<div> <h3>Title</h3> <span>0</span> </div> <div></div> <div></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.() -> Unit): String val html: String = renderToString { Div({ classes("card") }) { Text("Hello") Span({ classes("title") }) { Text("World") } } } // html == """<div>Hello<span>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(), 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.
Facts Only
* Kotlin is a multiplatform language developed by JetBrains.
* The vision is to allow backend developers to build server-rendered UI using type-safe, reusable Compose components instead of string templates.
* Compose Multiplatform targets Android, iOS, Desktop, and the web.
* Compose HTML uses the Compose runtime to build SPAs in Kotlin and compiles to JS.
* The proposal is to add a JVM target to Compose HTML to support SSR.
* A hypothetical `renderToString` function would compose the UI once on the JVM and serialize the tree to an HTML string.
* An example application shows how server-side logic can handle form submissions entirely within Compose functions.
* Existing Kotlin web solutions include Kobweb, Kilua, and Summon.
* The goal is for a hypothetical Spring or Ktor integration to turn `@Composable` functions directly into routed pages.
Executive Summary
The discussion centers on applying the principles of server-side rendering (SSR) to Kotlin's Compose Multiplatform UI toolkit, aiming to provide backend developers with type-safe, reusable components for building server-rendered HTML instead of relying on string-based templates. The core vision is to bring the composable paradigm—which spans Android, Desktop, and iOS—to the server environment. A key proposal involves extending Compose HTML with a JVM target to enable rendering functions (`renderToString`) that serialize the UI tree directly into an HTML string on the server without involving a browser DOM.
Existing work in the Kotlin web ecosystem includes frameworks like Kobweb (supporting static export) and Kilua (using the runtime for SSR/CSR), alongside existing attempts to address templating gaps in JVM development. The article posits that integrating Compose HTML's rendering capabilities could unify these separate approaches by providing a shared foundation for server-side UI generation across various Kotlin web frameworks. Future considerations involve handling hydration and state synchronization between the server-rendered output and client interactivity, while maintaining the principle of keeping Compose HTML focused on rendering rather than becoming a complete framework.
Full Take
Sentinel — Human
The text presents a reasoned argument exploring the potential of Kotlin Compose for Server-Side Rendering, building a conceptual bridge between existing patterns in other ecosystems and the JVM landscape through practical, illustrative sketches.
