How to Build a Cross‑Platform Java Desktop App with React and shadcn/ui
This article explains how to embed modern web UI built with React, TypeScript, and shadcn/ui into a Java desktop application using JxBrowser, covering reliable web view integration, server‑less resource loading, and bidirectional Java‑JavaScript communication via bridges or gRPC.
Swing/JavaFX Limitations
UI pain points: modern animations require custom implementation.
Ecosystem desert: few component libraries, low community activity, hiring difficulty.
Outdated look: default controls appear decade‑old.
Overall Solution: JxBrowser + React + shadcn/ui
The goal is a preferences dialog that persists settings to the local file system and restores them on restart.
Three core challenges:
Reliable web view: Java’s built‑in WebView lags behind modern web standards.
Server‑less loading: production must not depend on a local or remote server.
Java ↔ JS communication: file‑system operations should bypass a web server.
Window and Web View
Use a Swing JFrame as the native window and embed a Chromium‑based view provided by JxBrowser:
var engine = Engine.newInstance(HARDWARE_ACCELERATED);
var browser = engine.newBrowser();
SwingUtilities.invokeLater(() -> {
var view = BrowserView.newInstance(browser);
var frame = new JFrame("Application");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
engine.close();
}
});
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.add(view, BorderLayout.CENTER);
frame.setSize(1280, 900);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});Resource Loading: Development vs Production
During development a dev server provides hot‑reloading:
./gradlew startDevServer
if (!AppDetails.isProduction()) {
browser.navigation().loadUrl("http://localhost:[port]");
}In production package all web assets into the JAR and serve them via a custom jxb:// scheme using a request interceptor:
var options = EngineOptions.newBuilder(HARDWARE_ACCELERATED)
.addScheme(Scheme.of("jxb"), new UrlRequestInterceptor())
.build();
var engine = Engine.newInstance(options);Load resources:
if (!AppDetails.isProduction()) {
browser.navigation().loadUrl("http://localhost:[port]");
} else {
browser.navigation().loadUrl("jxb://my-app.com");
}The interceptor returns index.html, CSS, and JS files from the classpath, keeping all resources internal while normal HTTPS/API calls continue to work.
Java ↔ Web Communication
Two approaches to invoke Java from the web front‑end:
Option 1: JS‑Java Bridge (small projects)
@JsAccessible
class PrefsService {
void setFontSize(int size) { }
}
// TypeScript side
declare class PrefsService {
setFontSize(size: number): void;
}
prefsService.setFontSize(12);Simple but lacks compile‑time checks as the API grows.
Option 2: Protobuf + gRPC (large projects)
service PrefsService {
rpc SetFontSize(FontSize) returns (google.protobuf.Empty);
}
enum FontSize {
SMALL = 0;
DEFAULT = 1;
LARGE = 2;
}Run a gRPC server on the Java side and connect from the web side using a Connect client:
// Java server
class PrefsService extends PrefsServiceImplBase { /* implementation */ }
// TypeScript client
const transport = createGrpcWebTransport({ baseUrl: `http://localhost:50051` });
const prefsClient = createClient(PrefsService, transport);
prefsClient.setFontSize(FontSize.SMALL);The gRPC approach provides type safety, automatic code generation, IDE completion, and compile‑time validation.
References
GitHub repository with the full source code: https://github.com/TeamDev-IP/JxBrowser-Gallery
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
