Thoughts on Clojure UI framework

I had a long‑standing dream: to implement a UI framework. Nothing inspires me more than noticing hundreds of subtle interactions (e.g. text selection in a text box) and seeing how combined, they bring together a feel of an alive and native component.

For a long time, I thought it’s a Leviathan task: something for a hundred‑people team and tens of years. But then Flutter came along and showed that it’s actually very feasible to re‑implement from scratch the entirety of platform UI to the very last detail.

After that, I joined JetBrains and worked not on one, but two different UI frameworks, which, again, turned out to be a very doable task for a small team.

Lately, Clojurists Together and Roam Research agreed to sponsor this work. I just can’t keep ignoring the signs the Universe is sending me.

I have no framework code yet, but some foundations are laid out in Skija and JWM.

This post is my thoughts on the subject, some opinions I have and some questions I am not sure how to answer. It’s aimed to facilitate discussion, so please, share your thoughts!

Why cross‑platform

The very same CPU, memory, and graphics card have no problem executing Windows, Linux, or macOS apps. They don’t care. Yet you can’t run a macOS app on Windows, Linux on macOS, or Windows on Linux. This is not a fundamental property of software, it’s a stupid historical mistake and we should work as hard as we can to correct it.

Why desktop apps

Despite mobile dominance, desktop remains important for professional and productivity apps, and to build them we need tools.

Mobile has Flutter, Compose, SwiftUI, and desktop is... less advanced. Especially cross‑platform desktop.

Why not mobile and desktop together

They’re too different. I can’t imagine writing a single app that works both on desktop AND on mobile and is good at both. Two different UIs — sure, but in that case, mobile is already pretty well covered.

I plan to focus on the high‑quality desktop instead of finding a mediocre middle ground between desktop and mobile.

Why not web

The web started to get more suitable for apps probably since Gmail in 2004. It’s very convenient, yet still very limited and has shortcomings.

I think the fundamental problem of the web is that its APIs are too high‑level. Without low‑level APIs, simple things are sometimes stupidly hard to do, and you have to undo the stuff web is doing for you to get what you need. This is backward.

To top this off, the web is also too bloated, too unstable, OS integrations are too limited, and it puts a very hard limit on your performance.

In other words, the web is good for simple stuff, but not for anything complex because of lack of control.

And you don’t get to choose a programming language :(

Why not Electron?

Electron is pretty much web, but with better OS integrations. But it’s still full of compromises: you don’t have threads, you’re stuck with JavaScript, can’t do your own layout, render smooth 144Hz animations, etc. It adds 150Mb to your app package and makes it a memory hog.

Yet it has been a massive success. I think it means our industry is craving for a good desktop UI framework. Electron is a great step in the right direction but not the final form.

Native or custom

There are two classes of cross‑platform UI frameworks: ones that try to wrap native widgets (SWT, QT) and ones that draw everything themselves (Swing, JavaFX, Flutter).

I don’t really see a choice here, because I’ve never seen native widgets wrapped in cross‑platform abstractions that work. They always get a million little details wrong and still don’t feel native. QT might look decent on some Linux DEs but falls apart on other systems.

QT delivering a mix of native and custom widgets, mistreating native and creating a visual mess

On the other hand, the web has taught people not to care for OS‑native look and feel. Your app will be accepted on all three platforms no matter which fonts, colors, and button shapes you use, as long as it looks and feels good.

Why new framework

Well, I want to build UI in Clojure, and Clojure is limited to what Java has: Swing/AWT or JavaFX.

Swing/AWT is very old, has lots of shortcomings, and modern problems are solved on top of the old APIs. The downside of being in Java Core is that you can’t really evolve as the world changes because you can’t break or remove things.

JavaFX has learned a lot from Swing mistakes but has very limited graphics APIs and weird HiDPI and ClearType rendering issues.

Finally, declarative frameworks seem to be a good idea, but neither Swing nor JavaFX is declarative. There’s cljfx which is declarative but it’s based on JavaFX widgets and I don’t want to use those.

Why Clojure

Finally, the biggest reason I think this is a worthy idea is Clojure itself. Having worked on UIs in ClojureScript + Figwheel, live reload is a blessing, and Clojure has even a better story there. REPL + live reload + declarative UI framework is a match made in heaven. Anything else will have to try really hard to get even close to this combination.

Tweak and reuse

The web’s solution to customization is that each button has hundreds of properties you can tweak. You can set background, gradients, border radii, but if you want tricky behavior, you are out of luck. On the other hand, if you don’t want any of that, you still have to bear the weight and complexity of 100 default properties.

I am thinking of another way of approaching things. Somewhere deep in the Compose internals I once saw something like this (not verbatim):

fun MaterialButton(text) {
    Hoverable {
        Clickable {
            RoundedRectangleClip {
                RippleEffect {
                    SolidFill {
                        Padding {
                            Text(text)
                        }
                    }
                }
            }
        }
    }
}

(and they say Lisps have too many parentheses)

What struck me here was that:

a. Internals are perfectly composable with each other, and

b. It’s trivial to write your own button!

If I don’t want different corners, I just write my own button, using 6 out of 7 existing components and only replacing RoundedRectangleClip with my own implementation. Want gradient? Replace SolidFill with GradientFill, but keep the rest!

This creates a great benefit both for the library (built‑in buttons don’t need hundreds of properties to satisfy everyone) and for the users (they can meaningfully reuse parts from the standard library and only replace parts they don’t like).

Call it a Lego model, if you will. Perfectly composable and reusable chunks and an invitation to play with it.

First‑class rendering access

At some point, somebody has to draw the actual button. I think it would be great to have direct and first‑class access to override the rendering of anything and draw what you need directly. I can spend all day guessing what type of rounded corners you might need, or can give you the Skia canvas and let you do what you want.

Nothing hurts me more than seeing people try to render diagonals with “creative” use of border‑width. It just feels wrong:

The same goes for the layout, by the way.

Declarative model

I’ve been on board with React VDOM approach since 2014 when it first made its way into the Clojure community. I think it’s a fantastic model and a huge breakthrough in how we build UIs.

I also think it works twice as good with an immutable and data‑oriented language like Clojure, where you can load in and out parts of your application, keep the state and see changes update the UI live without reloading.

The approach is seeing even more adoption now, as Flutter, Compose and SwiftUI joined the hype train. And I don’t see the reason not to go in that direction either.

Why not immediate mode

Immediate mode sounds great in terms of simplicity and speed of development. Unfortunately, it has fundamental problems with layout (you only get one pass and can’t know the size of your content in advance), so retained mode it is.

I’d love to get as close as possible to the simplicity of it, though.

Data‑oriented

I find the particular approach Compose has chosen a step in the wrong direction.

In Compose, you don’t pass components around. Instead, you call side‑effecting functions that modify the global state somewhere.

var button = Button("Press me") // <- button already added to the dom
List(button) // <- impossible

This approach means that if someone has created a button, you can’t “hold onto it”: you can’t log, inspect, modify, throw it away. The shape of your code defines the resulting UI tree, not the shape of your values. And programming languages have been optimizing value manipulation, not code manipulation.

In contrast, in React components are values that you can save, pass and do whatever you want. That’s the approach I like.

Clojure‑native

All ClojureScript‑React wrappers have to transform Clojure values to React values somehow. The beauty of building a new UI framework is that it can be Clojure‑native and completely skip this step: stuff like Hiccup could be interpreted directly.

Layout

I have three main inspirations here. The first one is a one‑pass layout algorithm from Flutter:

It’s a brilliant system: simple, performant, easy to understand, easy to extend/hack around. This is important because we want people to intercept control and build their own layout logic for the components that are different enough from the components that will be shipped.

The second is Subform layout, which taught me that layout system can be beautiful and symmetric, the same units can be used for everything and that flexbox is not the pinnacle of human engineering.

The third is a notion that parents should position children, and spacing is a part of the parent’s layout, thus margins are considered harmful.

Defaults are part of the API

SwiftUI is notorious for shipping components that change defaults depending on the OS version and context they are used in.

What can you build if your foundation is unsteady? Not much. The approach SwiftUI is taking is to ask developers to update their apps every year.

We don’t play this way in Clojure. In Clojure, we want people to build apps that last tens of years without a single change.

The solution is simple: if we commit to some defaults (e.g. paddings, line heights, colors), you can consider them to be part of the stable API.

Text handling

Typography is constrained the most in the existing Java UI frameworks: there simply is no place in the API to specify a stylistic set or load a variable font.

Since I’ve worked with fonts a little bit, I am eager to include high-quality modern typography into the new framework.

At the very minimum, I want to get these things:

The latter means font size and line-height will probably work a little different than they do on the web, but it will be much easier to correctly and reliably center a text label inside a button.

Being pedantic

As a pedantic person, I want every little detail to be right. I believe that even small discrepancies can communicate a feeling of a poorly made, unreliable product.

Stuff I aim to get right:

Performance

Getting smooth animations is very important. I am aiming at 144 Hz, as this seems to be the most common “above 60” refresh rate for the next few years. No concrete plan here, just “don’t do stupid things and measure performance often” for now.

Startup time

The startup time of Clojure is bothering me, but maybe compiling to GraalVM or bundling a custom Clojure runtime could help — remains to be seen. There’s hope.

Full package

I imagine everything will come together as some sort of Electron but for JVM.

The goal is this: write your app in Clojure, package and distribute it as any other native desktop app.

Definition of done: nobody can tell the app is written in Clojure.

Your feedback

This is just a dump of everything that is in my head right now. Nothing is final, many things are vague, everything could change.

That’s why I want to start a discussion:

Share your thoughts! Reply on Twitter or drop me a letter.

UPD: feedback

I finally processed most (all?) of the feedback this post has accumulated. Thank you for it, I’ll be looking deeper into every link mentioned. Here’s the list:

Accessibility—I missed it in the original post, but yes, it will be accounted from the start. BTW any good resources on how to get it right/not get it wrong?

Localization (RTL)—Same.

Accessibility

State management

Styling

CSS in JS as a good idea (we won’t start from CSS separated from layout, but it’s good to not to repeat HTML’s mistake).

Adobe Design tokens

Registering a ‘defaults pack’ with a set of defaults, pass them around as arguments.

Layout

Flexbox/Grid as inspiration

Every Layout

Elm-UI and specifically these two talks:

Things to draw inspiration from

Further ideas

General concerns

Hi!

I’m Niki. Here I write about programming and UI design Subscribe

I consult companies on all things Clojure: web, backend, Datomic, DataScript, performance, etc. Get in touch: niki@tonsky.me

I also create open-source stuff: Fira Code, DataScript, Clojure Sublimed, Humble UI. Support it on Patreon or Github