Cljfx describes JavaFX as data, and re-renders it on the fx thread
Declarative, functional and extensible wrapper of JavaFX inspired by react and re-frame
At a glance
- What is it?
- Cljfx is a Clojure wrapper around JavaFX that builds interfaces from plain maps instead of builder DSLs, with a renderer that reconciles the description against the live scene graph. It fits Clojure developers who want a declarative JVM UI, and it does not fit a team that wants reactive atoms, a global event bus, or a non-JavaFX toolkit.
- Who is it for?
- Cljfx fits a Clojure team that wants to build a JVM desktop UI out of immutable data and functions and is willing to own the composition model it imposes, and it does not fit a team that needs re-frame's event and subscription architecture, or that wants a UI toolkit other than JavaFX.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 31 days ago.
- What is it written in?
- Mainly Clojure, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- cljfx/cljfx1,105 ★ · +2
| Repository | 2026-10-22 | 2026-11-07 |
|---|---|---|
| cljfx/cljfx | 1.1k | 1.1k |
2 Stars in the last 16 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Maps instead of hiccup, because JavaFX nodes have many children
Cljfx is a declarative, functional and extensible wrapper of JavaFX, inspired by the better parts of react, reagent, re-frame and fn-fx. The design decision that defines it is the shape of a component description. Components are described by maps with an :fx/type key, and the README is explicit about why: like react it lets you specify only the desired layout and handle the actual changes underneath, but unlike react, and unlike the web in general, it does not impose an xml-like structure on everything that can possibly have multiple children. Maps are used instead of hiccup for describing layout.
That is not a small detail. A JavaFX scene graph is a graph of nodes with many observable properties, not a tree of single-child elements, and hiccup's vector-of-children convention fights that. A map with :fx/type and named properties sidesteps the problem.
The :fx/type value itself has three allowed forms by default. It can be a keyword corresponding to some JavaFX class, in kebab-case, derived from the class name. It can be a function, which receives the map as an argument and returns another description, which is how custom components are written. Or it can be an implementation of the Lifecycle protocol, which is the documented extension point.
The other keys in the map represent JavaFX properties of that class, also in kebab-case, and if a property can be changed by the user, there is a corresponding :on-x-changed prop for observing that change.
The renderer reconciles a description against the live scene graph
The mental model the README gives for stateful UI is a renderer: a function you call whenever you want with a new description, and cljfx advances all the mutable state underneath to match that description. The call returns a derefable that will contain the component value with the most recent description.
The batching behaviour is the part that makes it usable. The renderer batches descriptions and re-renders views on the fx thread only with the last received description, so it is safe to call it many times at once. That matters on the JVM because JavaFX touches the scene graph from one thread only, and a library that made you marshal every state change yourself would push that problem into application code.
The example in the README is a small window with a close button, where the button's :on-action handler calls the renderer again with :showing false. Clicking close hides the window. The pattern generalises to a pure function from state to description, which is the same shape a react component has, except the output is a map rather than a virtual DOM node.
Because the renderer is a plain function holding the component, a second renderer or a second window is a second function, not a special case. The examples directory includes e10_multiple_windows.clj, which is where that assumption gets tested.
State lives in an atom that the renderer watches, not in reactive atoms
Cljfx offers a way to hold a single global state value in an atom, derive the description from that value, and change the atom instead of rebuilding the tree by hand. The example defines *state as an atom of a map, defines render functions that destructure it, and then wires the two together with a middleware that maps the incoming data to a component description.
The middleware is the piece that makes this readable. In the README example, the middleware is fx/wrap-map-desc assoc :fx/type root, so the incoming state is turned into a root component by a function call, and then fx/mount-renderer attaches the atom to the renderer so that edits to the input field update the displayed app title immediately.
This is where cljfx draws its line against reagent. The README says that unlike reagent, it rejects using multiple stateful reactive atoms for state and instead prefers composing the UI in a more pure manner. One atom, functions that read it, and a renderer that re-renders on change. The trade is that you give up the fine-grained reactivity of a deref inside a component, and gain a rendering model where the whole description is recomputed and then reconciled.
The examples folder carries this theme through, with e04_state_with_context.clj, e05_fn_fx_like_state.clj, e13_re_frame_like_state.clj and e15_task_tracker.clj as separate ways of holding state.
No global state and no hard-coded event bus, which is the re-frame difference
The comparison with re-frame is the most opinionated part of the project's design. Like re-frame, cljfx provides an approach to building large applications using subscriptions and events to separate view from logic. Unlike re-frame, it has no hard-coded global state, and subscriptions work on referentially transparent values instead of ever-changing atoms.
That is a genuine architectural difference rather than a naming one. In re-frame, the event layer and the subscription layer are part of the framework, so global state and subscriptions are baked in rather than left to the application. In cljfx, none of that is imposed. You can build a re-frame-like structure yourself, and the example e13_re_frame_like_state.clj exists for exactly that experiment, but the library itself will not hold your state for you.
The consequence for a team is that cljfx is smaller in scope and puts the application architecture decision back in your hands. If you are migrating from re-frame, you will find yourself rebuilding the layers re-frame gave you, with the benefit that none of them are hard-coded into the library.
Event handling is the other place the design pushes back. The README's map events section starts with a todo-view component and notes that there are problems with using functions, and the examples directory carries several attempts at it, including e18_pure_event_handling.clj and e03_map_event_handler.clj. Whether a given approach reads cleanly is the kind of thing you will have to judge by reading those files rather than by reading the overview.
The lifecycle protocol is the documented extension point
Cljfx describes itself as extensible, and the mechanism is the Lifecycle protocol. Beyond the two default forms of:fx/type, a value can be an implementation of that protocol, and the README points to the extending cljfx section for detail. The comparison with fn-fx is offered here: unlike fn-fx, cljfx is more dynamic, allowing users to use maps and functions instead of macros and deftypes, and it has a more explicit and extensible lifecycle for components.
That is the meaningful difference between the two approaches to wrapping a JVM UI toolkit. fn-fx uses macros and deftypes, which gives compile-time structure but ties a component to a deftype. Cljfx builds everything from runtime values, so a component can be assembled, inspected and extended at runtime, at the cost of not getting macro-level guarantees.
The examples directory makes the extension surface concrete. There is e21_extension_lifecycles.clj, e07_extra_props.clj for properties beyond the built-in ones, e14_charts.clj, e08_media.clj, e17_dialogs.clj, e23_accelerators.clj and e20_markdown_editor.clj. The scale of that list is a good proxy for what the library can actually reach, since JavaFX covers media playback, charts, dialogs, keyboard accelerators and rich text editing.
The repository layout supports the same idea. It carries separate jdk8 and jdk11 directories, a pom.xml for the maven publication, a build.clj and a deps.edn for the tools.deps workflow, and an example-projects directory alongside the smaller examples, so you can see both a single file and a fuller project layout.
Clojars, tools.deps, and the Java 8 versus Java 11 decision
Cljfx uses tools.deps, and the README gives the git coordinate with a placeholder sha to fill in:
cljfx {:git/url "https://github.com/cljfx/cljfx" :sha "<insert-sha-here>"}The library is also published on Clojars, so it can be added as a maven dependency instead, with the current version shown on the Clojars badge. The minimum required version of Clojure is 1.10.
The Java version depends on how you depend on it, and this is the detail most likely to bite. With git coordinates, the minimum required Java version is 11. With the maven dependency, both Java 8, assuming it has JavaFX provided in the JRE, and Java 11 via the openjfx dependency are supported, and correct classifiers are picked up automatically, so there is nothing to configure.
The README then tells you not to use JavaFX 8 anyway. It states that JavaFX 8 is outdated and has problems some people consider severe: it does not support HiDPI scaling on Linux, and sometimes crashes the JVM on macOS Mojave. The recommendation is to prefer JDK 11. So the Java 8 support path exists for compatibility, but the project's own advice is to run on JDK 11, which also means the git coordinate path is the one that matches the recommended setup.
Where Cljfx is the wrong tool
The boundaries here are mostly inherited from JavaFX itself, and it is better to name them than to discover them late. JavaFX is a desktop toolkit. It is the right choice for a JVM desktop application, and it is the wrong choice if the target is a browser, a mobile app, or a server-side render.
The second boundary is the composition model. Cljfx rejects multiple stateful reactive atoms, has no hard-coded global state, and does not ship re-frame's subscription and event architecture. A team that wants a large application with a formal event layer will spend effort either building that layer or finding that the library fights it.
Third, the renderer model has a cost. Because the whole description is recomputed and reconciled rather than patched in place, a very large tree re-rendered frequently is a different performance profile from an imperative scene graph. The README does not document profiling or performance characteristics, so a team with a heavy UI should measure this themselves rather than assume.
The project itself is in good shape. The licence is MIT, the last push was on 2026-10-08, and the release history shows 1.10.363 on 2026-10-08, 1.10.362 on 2026-09-15 and 1.10.361 on 2026-08-22, a cadence of frequent patch-level releases. The repository also has a CHANGELOG.md, a Slack channel for cljfx on the Clojurians workspace, and a continuous integration workflow for Clojure.
Editorial conclusion
Cljfx fits a Clojure team that wants to build a JVM desktop UI out of immutable data and functions and is willing to own the composition model it imposes, and it does not fit a team that needs re-frame's event and subscription architecture, or that wants a UI toolkit other than JavaFX. Before adopting it, decide whether JavaFX itself is acceptable on your target platforms, because the README warns that JavaFX 8 does not support HiDPI scaling on Linux and sometimes crashes the JVM on macOS Mojave, and prefer JDK 11, then add the cljfx coordinate with tools.deps using a git sha or take the Clojars maven artifact with a minimum Clojure of 1.10.
Frequently asked questions
How do I add Cljfx to a Clojure project?
Cljfx uses tools.deps, so you can add the git repository with the latest sha as a dependency, or take the maven artifact published on Clojars. The minimum required version of Clojure is 1.10.
What Java version does Cljfx need?
With git coordinates the minimum required Java version is 11, and with the maven dependency both Java 8 and Java 11 are supported with correct classifiers picked up automatically. The README recommends JDK 11, because JavaFX 8 does not support HiDPI scaling on Linux and sometimes crashes the JVM on macOS Mojave.
How are Cljfx components described?
Components are maps with an :fx/type key, whose value is a kebab-case keyword for a JavaFX class, a function that returns another description, or an implementation of the Lifecycle protocol. The remaining keys are JavaFX properties in kebab-case, with :on-x-changed props for properties the user can change.
Does Cljfx use a global state like re-frame?
No. The README states that cljfx has no hard-coded global state, and that subscriptions work on referentially transparent values instead of ever-changing atoms. The usual pattern is a single atom holding application state, a render function, and fx/mount-renderer to connect them.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cljfx-cljfx)Star history chart for your README
[](https://hysenlabs.com/projects/cljfx-cljfx)Was this useful?