A reactive UI library for Scala.js - built from 17 years of searching for clarity.
I have used Angular. React. Svelte. SolidJS. Dozens of others.
Each one promised simplicity. Each one delivered a different kind of complexity: hidden lifecycle management, implicit reactivity storms, state that moves in ways you cannot predict, and a growing feeling that the framework - not you - is in charge.
After 17 years I stopped looking for the right framework and built the thing I actually wanted.
Not a framework that does more. A framework that does less, but honestly.
JFX2 is a reactive UI library for Scala.js with a hard focus on three things:
Explicit lifecycles. Every component lives in a controlled scope. You know exactly when it starts, what it owns, and when it is gone. No hidden subscriptions. No memory leaks you discover six months later.
Predictable state. Property[T] is the core reactive primitive. It has no magic. It does not re-render your whole tree. It updates exactly what you tell it to update.
A declarative DSL that stays readable. UI structure is declared in Scala, close to the metal, without virtual DOM overhead and without the ceremony of a JSX transpiler.
Here is the kind of code JFX2 is trying to protect:
import jfx.action.Button.button
import jfx.core.component.Component.*
import jfx.core.state.Property
import jfx.layout.Div.div
import jfx.layout.VBox.vbox
val count = Property(0)
vbox {
classes = Seq("page")
div {
text = count.map(value => s"Count: $value")
}
button("Increment") {
onClick { _ => count.set(count.get + 1) }
}
}You can read this six months later and still nod at it.
JFX2 is not trying to hide the shape of the UI behind abstractions that need a manual.
It is trying to keep the shape visible.
The value is not just that this compiles. The value is that it still makes sense later.
| JFX2 | Laminar | Raw Scala.js DOM | |
|---|---|---|---|
| Lifecycle model | Explicit scopes | Signal-based | Manual |
| State | Property[T], predictable |
Signal/Var, reactive graph |
Manual |
| DSL | Declarative, JavaFX-inspired | Declarative, FRP-style | Imperative |
| SSR + Hydration | Yes | Limited | No |
| Forms + Validation | Built-in | Third-party | Manual |
| Virtual lists | Built-in | Third-party | Manual |
JFX2 is not better than Laminar. It is different. Laminar is excellent for functional-reactive programming. JFX2 is for developers who want structure over abstraction - explicit over implicit, scoped over ambient, readable over clever.
Every UI subtree lives in a component scope. When the scope ends, everything inside it - subscriptions, services, child components - is disposed deterministically.
That makes it safe to use inside virtualized lists, modals, and dynamically mounted routes without worrying about what you left behind.
No hidden dependency tracking. No surprise re-renders. You observe what you choose to observe.
The smallest useful screen should still feel calm.
The event model stays direct. The code reads like intent, not ceremony.
Property[T] is intentionally small.
There is no hidden graph to understand. If you observe it, you own it.
Services are registered and resolved within the current DSL context. No global singletons. No implicit parameters threading through your entire codebase.
Forms bind directly to model properties by name. Validation annotations live on the model, so the form does not need to be hand-wired field by field.
The form connects automatically. No manual wiring. No string-map choreography.
The same property-oriented model can be serialized and restored without hand-written mapping code.
That matters because the data model and the UI model are not treated as separate kingdoms.
Complex controls are still kept inside the same DSL.
That matters because the control is not a separate universe. It is still just a component with a lifecycle, state, and structure.
Routes are part of the component model too.
The important part is not syntax. It is that navigation, render state, and URL state remain legible.
Server HTML and client hydration share the same component structure. No duplicated templates. No DOM drift.
Large data sets should feel calm too.
This is not only a scrolling trick. Browser rendering, SSR rendering, hydration, route query state, and virtualized content all participate in the same component model.
JFX2 does not make artificial message keys the primary identity of text. The English source sentence stays visible in Scala.
The interpolator creates structured messages with placeholder names, source position, and fingerprinting for lookup and stale handling.
Property[T]andListProperty[T]- reactive primitivesRemoteListProperty[T, Q]- async list loading and range queries- Component scopes via
DslRuntime.withComponentScope DslRuntime.provide/DslRuntime.service- scoped dependency lookup- Layout DSL -
vbox,hbox,div,viewport,window,drawer Form- model binding and validation with annotationsRouter- async, nested, with path params and query stringsVirtualListView- efficient rendering of large datasetsJsonMapper- serialize/deserialize models withPropertyfields- i18n - message-centered, type-safe, multi-locale
Each module has its own README with installation notes and examples for the components or APIs it owns.
| Module | Purpose |
|---|---|
| jfx-core | Component lifecycle, DSL, state, layouts, SSR and hydration |
| jfx-router | Async routes, route context, query parameters and navigation |
| jfx-viewport | Global viewport, windows, notifications and anchored overlays |
| jfx-i18n | Source-first messages, catalogs, locale fallback and interpolation |
| jfx-json | Reflection based JSON serialization for Property models |
| jfx-controls | Link, image, table view and virtual list view |
| jfx-forms | Forms, inputs, validation, combo boxes, array forms and image cropper |
| jfx-editor | Lexical based rich text editor and editor plugins |
| jfx-webAuthn | Browser-side WebAuthn registration, authentication and passkey payload helpers |
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-core" % "2.3.0"
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-router" % "2.3.0"
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-viewport" % "2.3.0"
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-i18n" % "2.3.0"
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-json" % "2.3.0"
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-controls" % "2.3.0"
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-forms" % "2.3.0"
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-editor" % "2.3.0" // optional, includes Lexical
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-webauthn" % "2.3.0" // optional, browser-side passkeysRequires Scala 3.8.3, Scala.js, and ES module output targeting ES2021.
For local development in this repository:
sbtn-x86_64-pc-win32.exe scalajs-jfx2-core/test
sbtn-x86_64-pc-win32.exe scalajs-jfx2-controls/testIf you want the companion CSS package in the browser build:
npm install @anjunar/scalajs-jfx2Use JFX2 if you are building:
- complex data UIs - tables, dashboards, editors
- applications where lifecycle leaks are a real cost
- Scala backends where you want the same language on the frontend
- anything where you want to understand every moving part
Do not use it if:
- you want the smallest possible bundle for a simple page
- you prefer a purely functional reactive model
- you need a large ecosystem of third-party components today
- Published on Maven Central
- Used in production (Technology Speaks)
- Core patterns are stable
- Active development - feedback is welcome
This library is not an experiment. It is the result of a long and honest search for an architecture that I could trust - one where I know what is happening, where state lives, and what gets cleaned up.
Every decision in JFX2 is a choice for explicitness over magic. That comes with tradeoffs. It asks more of you upfront. In return it gives you something most frameworks do not: a codebase that is still legible and maintainable when it grows.
If that matters to you, you will feel at home here.
MIT - © 2026 Anjunar