I wrote a thing -- a Kotlin web framework called Kobweb.
It is built on top of Compose HTML, an official reactive web UI framework created by JetBrains (in close collaboration with Google, and in turn built upon core technologies introduced in Android's Jetpack Compose).
And this whole site, including this very page you are now perusing, is Kobweb's first user.
Frontend development in Kotlin is still in its early days, so it's an exciting time to explore the space. In this post, I'll introduce some Kobweb basics, as well as discuss why you might (or might not!) want to use it.
Compose HTML is a rich API that does an impressive amount of work wrapping underlying html / css concepts into a reactive API. However, it is ultimately a foundational layer, leaving many choices to the developer on how to approach the final design.
If instead you wanted to start writing Kotlin code immediately to create webpages, that's where Kobweb comes in.
Let's say you recently picked up the domain https://example.com
. One of the very first things you might want to do is create the page https://example.com/hello
.
With Kobweb, this couldn't be easier -- just annotate a composable method with the @Page
annotation, and you're done:
// src/jsMain/kotlin/com/example/pages/Hello.kt
package com.example.pages
@Page
@Composable
fun HelloPage() {
Text("Hello, World!")
}
That's it! Really!
To test it, spin up a Kobweb server (using kobweb run
), visit http://localhost:8080/hello
in your browser, and enjoy your working Kobweb site.
Once you have at least two pages, you can navigate between them using a Link
. The page transition will happen instantly, without needing to fetch additional information from the server.
In other words, if we add this "goodbye" page:
// src/jsMain/kotlin/com/example/pages/Goodbye.kt
package com.example.pages
@Page
@Composable
fun GoodbyePage() {
Text("Goodbye, Cruel World!")
}
we can then modify our "hello" page example to add a link to it:
// src/jsMain/kotlin/com/example/pages/Hello.kt
package com.example.pages
@Page
@Composable
fun HelloPage() {
Text("Hello, World!")
Link("/goodbye", "Say goodbye...")
}
With this setup, https://example.com/hello
will now show a link which, if clicked, will switch your page instantly to https://example.com/goodbye
.
If you pass an external address to Link
, e.g. Link("https://google.com")
, then it will act like a normal html link and navigate to that page as you'd expect.
Kobweb can be used on its own for its routing capabilities, but it also provides a UI library called Silk, a color-mode-aware (i.e. light and dark) collection of widgets as well as general theming and CSS styling support via CSS style blocks.
I believe CSS style blocks are one of those features that once you start using them, you won't want to go back. I demonstrate it later in its own subsection▼.
Did you happen to see the color toggling button at the top-right of the site? No need to move your cursor -- I'll create another copy here:
This button encapsulates the logic for changing this site's active color mode. Try clicking on it!
It's trivial to query your site's color mode. Silk exposes a ColorMode.current
property:
@Composable
fun SomeWidget() {
val colorMode = ColorMode.current
val widgetColor =
if (colorMode.isDark) Colors.Pink else Colors.Red
/* ... code that uses widgetColor ... */
}
You can also use var
instead of val
with ColorMode.currentState
in your code, if you want to change the color mode, not just read it:
@Composable
fun ToggleColorButton() {
var colorMode by ColorMode.currentState
Button(onClick = { colorMode = colorMode.opposite })
}
So far, most of this post has been text. But honestly -- text is static. How droll.
This is the future! Users of the web3 era demand more.
Using Kotlin, you can create dynamic elements by rendering to a canvas. The following clock is adapted from this Mozilla canvas example that was originally written in JavaScript.
It additionally renders differently depending on the site's color mode. You can click this color mode button to observe the results yourself.
Here's the Kotlin source.
Among other things, Silk provides a helpful Canvas2d
widget which makes it easy to register some code that will automatically get called for you once per frame.
@Composable
private fun Clock() {
Canvas2d(300, 300, minDeltaMs = ONE_FRAME_MS_60_FPS) {
/* This callback handles one frame of canvas rendering. */
}
}
Despite being easy to use, the canvas widget is extremely powerful, and you could use it to create dynamic effects, full screen backgrounds, or even games.
Anyone who has dabbled with Jetpack Compose is likely familiar with the Modifier
class. It may seem as fundamental to Compose as the @Composable
annotation is.
However, it isn't! Compose HTML actually does not have a Modifier
class.
Instead, it uses an approach where all HTML tags are converted to @Composable
function calls that take in something called an AttrsScope
.
As a concrete example, this HTML document tag:
<div
id="example"
style="width:50px;height:25px;background-color:black"
>
would be written with the following Compose HTML code:
Div(attrs = {
assert(this is AttrsScope)
id("example")
style {
width(50.px)
height(25.px)
backgroundColor("black")
}
})
I think this approach is pretty neat, but as AttrsScope
is a mutable class, that makes it dangerous to store in a shared variable. Plus, its API doesn't support chaining.
To solve this, Silk provides its own Modifier
class which is inspired by Jetpack Compose's version but isn't exactly the same one. Still, it should look familiar enough to people who write Jetpack Compose code.
The above Compose HTML AttrsScope
would be represented by the following Modifier
:
private val EXAMPLE_MODIFIER = Modifier
.id("example")
.width(50.px).height(25.px)
.backgroundColor(Colors.Black)
Silk widgets take modifiers directly:
Button(
onClick = { /*...*/ },
modifier = EXAMPLE_MODIFIER
)
But for interoperability with Compose HTML elements, it is easy to convert a Modifier
into an AttrsScope
on the fly, using the toAttrs
method:
Div(attrs = EXAMPLE_MODIFIER.toAttrs())
With Modifier
s, chaining is easy using the then
method:
private val SIZE_MODIFIER = Modifier.size(50.px)
private val SPACING_MODIFIER = Modifier.margin(10.px).padding(20.px)
private val COMBINED_MODIFIER = SIZE_MODIFIER.then(SPACING_MODIFIER)
Modifiers are used heavily throughout Silk, which should help ease the experience for Android and desktop Kotlin developers just getting started with frontend development.
Most frontend projects have a single, giant, terrifying stylesheet (or, worse, several giant, terrifying stylesheets) driving the look and feel of their site.
Aside: If you don't know what a stylesheet is, it's a collection of CSS rules that target various elements on your page, specifying their style using a declarative format.
For example, at one point while working on Kobweb, I used a todo app to learn from, and at least half of the time I spent was crawling over their stylesheet to understand the nuances of their approach.
Compose HTML allows you to define this stylesheet in code, but you can still easily end up with a monolith.
Kobweb introduces CSS style blocks, which is a fancy way of saying you can define the styles you use in smaller pieces next to the code that uses them.
It's easy -- just instantiate a CssStyle
and store the result to a val
:
val HoverContainerStyle = CssStyle {
base { Modifier.fontSize(32.px).padding(10.px) }
hover {
val highlightColor =
if (colorMode.isDark) Colors.Pink else Colors.Red
Modifier.backgroundColor(highlightColor)
}
}
The base
style, if defined, is special, as it will always be applied first. Any additional declarations are layered on top of the base if their condition is met.
CSS styles can be converted to Modifier
s using the toModifier
method and to AttrsScope
s using the toAttrs
method. This way, you can pass them into either Silk widgets or Compose HTML elements:
val HoverContainerStyle = CssStyle { /*...*/ }
// Then later...
// Silk widget:
Box(HoverContainerStyle.toModifier()) { /*...*/ }
// Compose HTML element:
Div(attrs = HoverContainerStyle.toAttrs()) { /*...*/ }
It is way easier to read your code when your element styles live near where they are used, since you don't have to jump between the code and a monolithic stylesheet in a different file.
At the beginning of this post, I said this site was written entirely in Kotlin. This may actually be a technicality.
In fact, most of this site is written using markdown. Relevant markdown files are transpiled to Kotlin just before compilation happens.
Kobweb extends Markdown with some custom support for nesting code inside it which is how I embedded the color buttons and clock widget above. You can inline code with a Kotlin-y ${...}
syntax or put a larger widget on its own line with triple curly-brace syntax:
# An intro to pathfinding
Here is a demonstration of A-star pathfinding
{{{ .components.widgets.astar.Demo }}}
Play: ${.components.widgets.astar.PlayButton}
Step: ${.components.widgets.astar.StepButton}
Code references that start with .
will automatically be prefixed by your project's base package, so for example all the code references above would generate final code prefixed with something like com.example
(but whatever is used by your project).
You can see the markdown for this blog post for yourself!
Ultimately, Markdown support out-of-the-box means that if you love Kotlin and you were thinking of starting a blog, Kobweb might be a great solution for you.
Let's finish off by discussing other approaches, to compare and contrast with Kobweb.
If you're already sold on Kobweb, feel free to skip this section and jump straight to the conclusion▼.
Many users in the Kotlin community are excited about the promise of multiplatform, and they want to write an app once and run it everywhere (Android, iOS, Desktop, and Web).
Kobweb is very much not that sort of solution. It is designed for developers who want to create a traditional website but use Kotlin instead of, say, TypeScript.
Kobweb (and Compose HTML) interact with the DOM and let the browser handle rendering it. In contrast, Compose Multiplatform for Web works by creating an HTML canvas and then rendering your app to it opaquely. If what you really want to do is write a cross-platform app which just happens to also work in your browser, then Compose Multiplatform is probably the solution for you.
There's no one-size fits all solution, however, and Kobweb may still be the right choice if you're creating a website. I wrote about this a bit more in Kobweb's README, in case you wanted to learn more about the different approaches, as well as why you might still choose a traditional DOM API in a multiplatform world.
Perhaps you've been burned by frameworks before. "Yeah buddy, Kobweb is nice, but I'm just going to stick with Compose HTML classic."
That's fine with me! Just be aware, this post only scratched the surface of what Kobweb can do for you. Here's a fuller list of features we provide, since if you go it alone, you may need to implement some of them yourself:
Modifier
concept, useful for chaining stylesBox
, Column
, and Row
on top of html / css/posts?userId=...&postId=...
)/users/{userId}/posts/{postId}
)I mention these not (just) to humblebrag, but because I myself was surprised by what was needed to create an MVP of Kobweb. I vastly underestimated the scope.
So, sure, I'm biased, but my opinion is that if you're going to use Compose HTML to make a website, you probably want to at least give Kobweb a try.
Kotlin/JS may not be for everyone. Most of the webdev community is amassed around JavaScript / TypeScript and libraries like React.
There are a lot of advantages to sticking with the crowd in this case. And not just because they have a huge headstart. Compile times tend to be a lot faster, you can experiment with JavaScript by typing commands directly in your browser, you'll benefit from a ton of community support and resources, and there's no shortage of interesting projects out there to learn from.
I have talked to many TypeScript programmers who vouch for it and say they enjoy writing code in the language. Microsoft has really done a great job adding seatbelts, helmets, and full body cushions to JavaScript (which itself is still evolving and getting better over time).
While I personally want to encourage more Kotlin developers to explore the frontend world and grow the community, if a new programmer came up to me today saying they wanted to write a website from scratch, especially with the hopes of developing skills that will turn into a frontend career, then I would send them to JavaScript / TypeScript tutorials at this point.
If you like what I'm doing with Kobweb but think at this time it makes more sense to use JavaScript / TypeScript for your project, check out Next.js paired with Chakra UI, as both of these solutions were huge inspirations for me.
I've been very excited about the Kotlin webdev space ever since Compose HTML was announced, and I hope this post has pushed at least one other person over the fence.
If Kobweb looks like something you'd want to play with, the easiest way to start is by installing the Kobweb binary.
Once installed, you can run:
$ kobweb create app
# answer a bunch of questions about your project
$ cd app/site
$ kobweb run
Or if this post just made you curious about Compose HTML, you can start with the official tutorial but have Kobweb set it up for you in a few seconds:
$ kobweb create examples/jb/counter
$ cd counter/site
$ kobweb run
And finally, if after reading this you are thinking about using Kobweb, consider jumping into our Discord server, where I'd be happy to answer questions about Kobweb when I'm around.
I can't predict if Kotlin webdev will ever take off, much less Kobweb itself. But I sincerely want a future where there are more Kotlin developers owning codebases that cross the full stack. If I can help throw some code over the wall to help make the experience better, then I'm happy to have tried.
At the very least, Kobweb will always have one user -- this site!