Build live, interactive web UIs in pure Kotlin — no separate frontend, no JavaScript, no REST glue.
SpringReact is a Spring Boot framework that lets you write your entire web UI as Kotlin server components. You describe a screen in Kotlin; the framework renders it, serves it, and keeps it live over a single WebSocket — when a user interacts, your Kotlin code runs and only the changed parts of the page update. It bundles its own React runtime inside the jar, so there is no separate frontend project and nothing to install on the client.
🟢 New to programming? Here's the idea in plain words: a modern website is usually two programs — a backend (on the server, holds your data) and a frontend (in the browser, the buttons people see) — plus glue to connect them. SpringReact removes the second one: you write everything once in Kotlin and it shows up, live, in the browser. It does assume you know a little Kotlin or Java — if you're brand new, start with Getting Started.
- Why SpringReact
- Quick start
- How a screen looks
- Features
- Documentation
- Examples
- Tailwind & npm modules
- Configuration
- Project structure
- Building from source
- Roadmap
- Contributing
- License
| Classic SPA + REST | Thymeleaf / server templates | SpringReact | |
|---|---|---|---|
| Languages | Kotlin + JS/TS | Kotlin + HTML templates | Kotlin only |
| Frontend project | Separate (npm, bundler) | None | None (runtime bundled in the jar) |
| Client ↔ server | Hand-written REST API | Full page reloads | Automatic, over one WebSocket |
| Live updates | DIY (state, fetch, sockets) | ✗ | Built-in (only changed DOM updates) |
| SEO / first paint | CSR (often empty source) | ✓ | ✓ server-side rendered |
You get React's interactivity with the simplicity of server-side development — one language, one process, one deployable jar.
Add the Gradle plugin — it configures Kotlin, Spring Boot, dependency management, and the framework:
// build.gradle.kts
plugins {
id("com.vexora.springreact") version "0.1.0"
}Or scaffold a new project from scratch:
./tools/create-springreact.sh my-app com.acme.myapp
cd my-app && ./gradlew bootRun # → http://localhost:8080See The Gradle Plugin and Getting Started.
@LiveComponent("Home") // a screen named "Home"
@Route("/") // served at the homepage "/"
class HomeScreen : ServerComponent {
@LiveState var count = 0 // state lives on the server
@LiveAction fun click() { count++ } // an action the user can trigger
override fun render(): UiNode = // what the screen looks like
div(
h1("You clicked $count times"),
button(onClick("click"), "Click me"),
)
}Open the page → "You clicked 0 times". Each click runs click() on the server, count
goes up, and the heading updates instantly — no API to write, no client state to manage.
New to this? Every line is explained in Getting Started.
- Server components —
@LiveComponent+render()in Kotlin, with full Spring DI. State lives in the JVM (@LiveState); events are@LiveActions. - One WebSocket transport — no REST, no client store. Full tree on mount, minimal diff patches after that.
- Routing & client navigation —
@Route("/path", layout, title); dynamic params (/users/{id}→@LiveParam), per-route titles, client-side nav (no full reload). - Layouts (incl. nested) —
Html.slot();@Layout(parent="…")nests layouts; they stay mounted while the inner screen swaps. - Realtime broadcast —
LiveBroadcaster.broadcast("Component")re-renders every connected client (live dashboards, presence, chat). - Forms + validation —
onSubmitbinds named fields to a typed Kotlin DTO, validated with Bean Validation before your action runs. - Keyed reconciliation —
key()on list children → minimal patches on reorder/insert/remove. - Custom widgets — drop real React components (charts, three.js, maps) into a screen with
widget("Name", …); logic stays on the server. - Authorization —
@LiveAuthorize("ROLE")+ a pluggableLiveSecuritybean (bridge to Spring Security). - Async, loading & redirects —
LiveContextgives an action ahandle()to push updates after background work, plusredirect("/path"). - 404 & error boundaries — render your own components for unknown URLs and render failures.
- Middleware —
LiveInterceptorbeans run before every action (logging, tenancy, rate limits). - Lifecycle & context —
onMount/onUnmount(reliable on disconnect) and access to headers/cookies/principal/locale. - i18n — locale from
Accept-Language+ SpringMessageSource. - Server-side rendering — initial screen + layouts pre-rendered to HTML on the JVM (no Node) for SEO and fast first paint.
- Bundled runtime — the React runtime is esbuild-bundled into the jar; consumers ship no frontend files.
One page per feature, beginner-friendly with copy-paste examples. New here? Read in order; or jump to what you need.
Basics
- Getting Started — your first screen in 5 minutes
- Server Components —
@LiveComponent,@LiveState,@LiveAction - The HTML DSL — building UI in Kotlin
- Routing & Layouts —
@Route, dynamic params, nested layouts, 404 - Forms & Validation — typed forms with error messages
Interactivity
6. Realtime Broadcast — push updates to every user
7. Custom Widgets — drop real React components into a screen
8. Authorization — guard actions by role
9. Lists & Keys — efficient list updates
13. Async, Loading & Redirects — slow work, server-side navigation
14. Middleware — run logic before every action
15. Lifecycle & Presence — onMount/onUnmount, who's-online
16. Internationalization — render in the user's language
Styling, npm modules & rendering
- 🎨 npm Modules & Tailwind — use Tailwind, three.js, charts, any node module
- SSR & Styling — server-rendered HTML + adding CSS
Setup, ops & reference
10. Configuration — every spring.react.* setting
17. The Gradle Plugin — one-line project setup
18. Publishing — release to Maven Central
12. How It Works — the architecture, end to end
- Frontend for a REST API — use SpringReact with your existing Spring Boot API
Tutorial 11. Build a Todo App — a complete app, step by step
| Example | What it shows |
|---|---|
examples/minimal |
The smallest app — one screen via the Gradle plugin |
examples/todo |
Full showcase: validated form, keyed list, quantity steppers, duplicate-name toast, filters, live badge, custom CSS, and a three.js custom widget |
examples/todo-api |
SpringReact as the frontend for a REST API — a /api/todos JSON API consumed by the UI via RestClient |
./gradlew publishToMavenLocal # make the framework available locally
cd examples/todo && gradle bootRun # → http://localhost:8080You write screens in Kotlin, so there is no frontend project by default. When you want the npm ecosystem there are two simple paths — full guide: npm Modules & Tailwind.
CSS / Tailwind — Tailwind emits a stylesheet; your class names live in Kotlin cls("…")
strings, so point Tailwind at your .kt files and load the output:
// tailwind.config.js — scan your Kotlin screens for class names
content: ['../src/main/kotlin/**/*.kt']spring.react.stylesheets=/app.css # built by `npx tailwindcss -o .../static/app.css`JS libraries (three.js, chart.js, …) — a client-side lib goes in a small widget bundle.
The runtime exposes its React on window.SpringReact.React, so your bundle shares it instead
of shipping a second copy:
import * as THREE from 'three'
function Cube() { /* … */ }
window.SpringReact.registerWidget('Cube', Cube)spring.react.scripts=/widgets.js # built with esbuild --alias:react=./react-shim.jswidget("Cube", attr("color", "#9b6dff"), attr("size", 180)) // use it from KotlinTrade-off: pure-Kotlin UI needs zero npm; the moment you use a JS library you need a small bundle step for that widget only. The
examples/todoAbout page renders a real three.js cube exactly this way.
All settings live under spring.react.* in application.properties / .yml. Everything has a
sensible default — zero config required.
| Property | Default | Meaning |
|---|---|---|
spring.react.title |
SpringReact |
default <title> |
spring.react.ssr |
true |
pre-render the initial screen into the HTML |
spring.react.stylesheets |
(empty) | CSS URLs added as <link> to the shell |
spring.react.scripts |
(empty) | JS URLs (widget bundles) loaded after the runtime |
spring.react.not-found-view |
(empty) | component to render for 404s |
spring.react.error-view |
(empty) | component to render when render() throws |
spring.react.allowed-origins |
* |
/live WebSocket origin allowlist |
spring.react.runtime-path |
/springreact/springreact.js |
URL of the bundled runtime |
SpringReact/ (Kotlin, build.gradle.kts)
├── src/main/kotlin/com/vexora/springreact/
│ ├── jsc/ Html DSL, UiNode/Element/Text/Attr, ServerComponent, UiTreeDiff, UiHtml (SSR)
│ ├── live/ @LiveComponent/@LiveState/@LiveAction, WebSocket handler, LiveBroadcaster,
│ │ LiveContext, LiveInterceptor, LiveSecurity, auto-config
│ ├── web/ @Route/@Layout + RouteRegistry, ReactView/ReactViewResolver
│ └── autoconfigure/ ReactProperties, ReactRenderer (shell + SSR), ReactAutoConfiguration
├── src/test/kotlin/ integration tests — drive the real /live WebSocket
├── gradle-plugin/ the `com.vexora.springreact` Gradle plugin (one-line setup)
├── client/ bundled runtime (esbuild): ServerView, Router, hooks, patch apply, widgets
├── examples/ minimal + todo apps
└── docs/ one guide per feature
Requirements: JDK 21, Node 22+. One command builds everything and runs both test suites:
./gradlew build- 26 Spring integration tests drive the real
/liveWebSocket (DI, routing, forms, broadcast, keyed diffing, auth, SSR, …). - 24 client tests (vitest + jsdom) cover patch application, routing, and full
ServerView/Routerrendering — plus a TypeScript typecheck.
The client runtime is esbuild-bundled into the jar automatically; there are no manual npm steps. See CONTRIBUTING.md.
- ✅ Server-side rendering (JVM-native, no Node)
- ✅ Gradle plugin + project initializer
- ✅ Custom widgets sharing the framework's React (three.js, charts)
- ✅ Maven Central publish wiring (signing + sources/javadoc, gated on credentials)
- ⬜ Publish to Maven Central & the Gradle Plugin Portal
- ⬜ Streaming SSR for fully server-rendered widgets
Contributions are welcome — see CONTRIBUTING.md. In short: ./gradlew build
must be green, framework code is Kotlin-only, and every feature ships with a doc page.