SSR · WCAG 2.2 AA · i18n · Themes · CMS · Reactive · Animations · Spring Boot
Why JUX? · Quick Start · Architecture · Modules · Examples · Configuration · License
JUX is a concept framework. It was born from a simple frustration: I'm tired of juggling HTML, CSS, JavaScript, Thymeleaf, npm, webpack, and twelve other things just to build a website on Spring Boot. So I built JUX — one language, one build system, one runtime. It works. It compiles. It renders. It ships. It's not final. It will evolve.
— XDSSWAR
Every Java web framework today makes you step outside Java. Thymeleaf? HTML templates with magic syntax. Vaadin? Generates JS behind your back. JSP? Dead. React + Spring? Two build systems, two languages, two mental models, and a REST API just to glue them together.
JUX says no to all of that.
You write Java. JUX renders HTML. Your pages are classes. Your elements are objects. Your styles are annotations. Your routes are annotations. Your i18n is type-safe interfaces. Your accessibility is enforced at the framework level. Your client-side interactivity compiles from Java to JavaScript via TeaVM.
@Route("/")
@Title("Welcome")
@Css("themes/default.css")
public class HomePage extends Page {
@Override
public Element render() {
return main_().children(
h1().text("Hello from JUX"),
p().text("Built entirely in Java. No templates. No JS. No CSS files.")
);
}
}That's a complete page. Spring Boot auto-configuration picks up @Route, registers the handler, renders SSR HTML, injects CSS. No controllers. No templates. No configuration classes.
| Feature | How it works |
|---|---|
| Server-Side Rendering | Component trees serialize to HTML5 — target < 5ms per page |
| Zero HTML templates |
Element tree API: div(), h1(), section(), nav() — all Java |
| Zero JS authoring | Client interactivity via TeaVM — Java compiles to JS, DOM access via org.teavm.jso.dom.html
|
| Zero CSS files | Theme system with design tokens, CSS variables, dark mode — generated from Java config |
| WCAG 2.2 AA by default | No img() without alt text. Audit engine catches violations at render time. ADA compliance built in. |
| Type-safe i18n | Translations are Java interfaces, not .properties files. Compile-time safety. |
| Cookie-based themes | Light/dark switching without page reload — data-theme attribute + CSS selectors |
| Reactive properties | Optional — observable values, bindings, computed expressions, observable collections |
| Animations | Optional — keyframe timelines, transitions (fade, scale, slide, rotate), easing functions |
| HTML templates | Optional — load HTML files into components via @Html, @HtmlId, @Slot annotations |
| Route engine |
@Route("/blog/{slug}") with typed path params, query params, security roles, caching |
| Spring Boot native | Auto-configuration, DI, JPA, Security — all the Spring ecosystem, zero friction |
| CMS module | Database-driven pages, widget registry, drag-and-drop admin panel |
JUX Demo — Bootstrap 5, light/dark themes, i18n
JUX Store — Tailwind CSS, e-commerce storefront
Client-Side Demo — TeaVM interactive components & animations
Gradle (Groovy DSL):
// build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.10'
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'io.github.xdsswar:jux-server:1.0.0'
implementation 'io.github.xdsswar:jux-themes:1.0.0' // optional: theme components
implementation 'io.github.xdsswar:jux-cms:1.0.0' // optional: CMS module
implementation 'io.github.xdsswar:jux-reactive:1.0.0' // optional: reactive properties & collections
implementation 'io.github.xdsswar:jux-animations:1.0.0' // optional: keyframe animations & transitions
implementation 'io.github.xdsswar:jux-html:1.0.0' // optional: HTML template loading
}Maven:
<dependencies>
<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-server</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-themes</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Optional modules -->
<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-cms</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-reactive</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-animations</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-html</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>@SpringBootApplication
@Css(value = "/css/styles.css", order = 1)
@Favicon(value = "/img/favicon.png", type = "image/png")
public class MyApp implements WebApplication {
public static void main(String... args) {
SpringApplication.run(MyApp.class, args);
}
}The WebApplication interface marks your main class as a JUX application. @Css, @Js, and @Favicon declared here apply to every page automatically — no need to repeat them.
package com.example.pages;
import xss.it.jux.annotation.*;
import xss.it.jux.core.*;
import static xss.it.jux.core.Elements.*;
@Route("/")
@Title("Home")
public class HomePage extends Page {
@Override
public Element render() {
return main_().children(
h1().text("It works."),
p().text("Your first JUX page.")
);
}
}# application.yml
jux:
base-package: com.example.pages./gradlew bootRunOpen http://localhost:909X. Done. (Check each demo application.yml file for the port)
┌──────────────────────────────────────────────────────────────────────┐
│ Consumer Project │
│ @Route pages · @JuxComponent widgets · @MessageBundle i18n │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────────────┐ │
│ │ jux-server │ │ jux-themes │ │ jux-cms │ │
│ │ SSR engine │ │ Components │ │ Page builder, widgets │ │
│ │ Route engine│ │ Design │ │ Admin panel │ │
│ │ Spring auto │ │ tokens │ │ Widget registry │ │
│ └──────┬───┬──┘ └──────┬──────┘ └───────────┬──────────────────┘ │
│ │ │ │ │ │
│ ┌──────┴───┴────────────┴──────────────────────┘ │
│ │ │
│ │ ┌───────────┐ ┌───────────┐ ┌─────────────┐ │
│ │ │ jux-core │ │ jux-a11y │ │ jux-i18n │ │
│ │ │ Element │ │ WCAG 2.2 │ │ Messages │ │
│ │ │ PageMeta │ │ Audit │ │ Locale │ │
│ │ │ Page │ │ AutoFix │ │ RTL │ │
│ │ └─────┬─────┘ └───────────┘ └─────────────┘ │
│ │ │ │
│ │ ┌─────┴──────────────┐ ┌──────────────┐ │
│ │ │ jux-annotations │ │ jux-processor │ │
│ │ │ @Route @Css @Title│ │ Compile-time │ │
│ │ │ @Meta @Js @Layout │ │ validation │ │
│ │ └────────────────────┘ └──────────────-┘ │
│ │ │
│ │ ┌─────────────┐ │
│ │ │ jux-client │ TeaVM: Java → JavaScript │
│ │ │ DOM bridge │ Hydration, state, events │
│ │ │ Event bind │ org.teavm.jso.dom.html.* │
│ │ └─────────────┘ │
│ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ OPTIONAL MODULES │ │
│ │ │ │ │
│ │ │ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ │ │
│ │ │ │ jux-reactive │ │ jux-animations │ │ jux-html │ │ │
│ │ │ │ Properties │ │ Timeline │ │ @Html parse │ │ │
│ │ │ │ Bindings │ │ Keyframes │ │ @HtmlId │ │ │
│ │ │ │ Collections │ │ Transitions │ │ @Slot inject │ │ │
│ │ │ │ Expressions │ │ Interpolators │ │ #{} i18n │ │ │
│ │ │ └──────────────┘ └────────────────┘ └──────────────┘ │ │
│ │ └────────────────────────────────────────────────────────────┘ │
│ │ │
├──┴───────────────────────────────────────────────────────────────────┤
│ Spring Boot 3.5.10 │
│ Web MVC · JPA · Security · DI · Cache │
└──────────────────────────────────────────────────────────────────────┘
GET /es/blog/hello-world?ref=twitter
→ DispatcherServlet
→ JuxRouteHandlerMapping matches /blog/{slug} (@Localized)
→ Security check @Route(roles = { ... })
→ Locale resolution URL prefix "es" → Locale("es")
→ Theme resolution Cookie "jux-theme" → "dark"
→ Cache check key = path + query + locale + theme
→ Page instantiation Spring DI (services via constructor injection)
→ Request init pathParams, messages, context injected into Page
→ Parameter injection @PathParam slug, @QueryParam ref
→ pageMeta() Dynamic title, OG tags, redirects
→ Metadata merge Annotations + programmatic, dedup
→ render() Element tree → HTML5 string
→ Accessibility audit WCAG 2.2 AA (dev mode)
→ Cache store Caffeine TTL
→ Response text/html, data-theme, lang, dir
JUX has two base classes with a clear separation of concerns:
Component |
Page |
|
|---|---|---|
| Purpose | Reusable UI building blocks | Routable views with @Route
|
| Methods | render() |
render() + pageMeta() + request access |
| Examples | Navbar, Footer, Card, Modal | HomePage, BlogPostPage, SearchPage |
| Instantiation |
new Navbar(data) — direct Java |
Spring-managed — auto DI |
| Request access | No (receives data via constructor) | Yes — pathParam(), queryParam(), messages(), locale(), context()
|
Component — a pure UI building block. Only has render(). No request awareness. Data flows in through the constructor:
public class ProductCard extends Component {
private final String name;
private final String price;
public ProductCard(String name, String price) {
this.name = name;
this.price = price;
}
@Override
public Element render() {
return article().cls("card").children(
h3().text(name),
span().cls("price").text(price)
);
}
}Page — a routable view. Has pageMeta() for metadata, plus built-in access to the entire request lifecycle:
@Route("/products/{id:long}")
@Title("Product")
public class ProductPage extends Page {
@PathParam private long id;
private final ProductService products;
public ProductPage(ProductService products) { // Spring injects this
this.products = products;
}
@Override
public PageMeta pageMeta() {
var product = products.findById(id);
return PageMeta.create()
.title(product.getName())
.ogImage(product.getImage())
.htmlLang(locale().getLanguage());
}
@Override
public Element render() {
var product = products.findById(id);
return main_().children(
new ProductCard(product.getName(), product.getPrice()).render(),
p().text(messages().getString("product.details"))
);
}
}Notice: no @Autowired needed. Pages are Spring beans — constructor injection works automatically with Spring Boot 3.x. And messages(), locale(), pathParam() are inherited from Page, not injected.
Components are plain Java objects. Compose them with new and call .render():
@Route("/about")
@Title("About Us")
public class AboutPage extends Page {
@Override
public Element render() {
return new PageLayout("/about", messages(), aboutContent()).render();
}
private Element aboutContent() {
return section().children(
h1().text("About Us"),
p().text("We build things with Java.")
);
}
}public class PageLayout extends Component {
private final String activePath;
private final JuxMessages messages;
private final Element content;
public PageLayout(String activePath, JuxMessages messages, Element content) {
this.activePath = activePath;
this.messages = messages;
this.content = content;
}
@Override
public Element render() {
return div().cls("page").children(
skipNav("main-content", "Skip to main content"),
new Navbar(activePath, messages).render(),
main_().id("main-content").children(content),
new Footer(messages).render()
);
}
}Data flows down through constructors. No magic injection. No global state. Just Java.
All the annotations that power the framework. No runtime dependencies.
@Route("/blog/{slug}") // URL mapping with typed params
@Title("#{blog.title}") // Page title (supports i18n keys)
@Css("pages/blog.css") // CSS resources
@Js(value = "chart.js", defer = true) // JS resources
@Meta(name = "robots", content = "index,follow")
@Layout(MainLayout.class) // Wrapping layout component
@Localized // Auto locale-prefixed URLs
@Favicon("/favicon.ico") // Favicon
@Canonical // SEO canonical URLParameter injection (on Page fields):
@PathParam private String slug; // /blog/{slug}
@QueryParam(defaultValue = "1") int page; // ?page=2
@HeaderParam("Accept-Language") String lang;
@CookieParam("session-id") String session;
@SessionParam("cart") ShoppingCart cart;
@RequestContext JuxRequestContext ctx; // Full request access
@LocaleParam Locale locale; // Resolved localeClient-side lifecycle:
@JuxComponent(clientSide = true) // Compiles to JS via TeaVM
@State private int count = 0; // Reactive — triggers re-render
@On("click") void handle(...); // DOM event binding
@OnMount void init(); // After hydration
@OnUnmount void cleanup(); // Before removalComponent — Base class for reusable UI building blocks:
public abstract class Component {
public abstract Element render(); // Build the UI tree
}Page — Base class for routable pages (extends Component):
public abstract class Page extends Component {
public PageMeta pageMeta() { ... } // Optional dynamic metadata
// Built-in request access — no injection needed
protected String pathParam(String name) { ... }
protected String queryParam(String name) { ... }
protected JuxMessages messages() { ... }
protected Locale locale() { ... }
protected String header(String name) { ... }
protected String cookie(String name) { ... }
protected <T> T session(String name) { ... }
protected boolean isPost() { ... }
protected String formParam(String name) { ... }
protected void status(int code) { ... }
protected void redirect(String url) { ... }
// ... more
}Element — Virtual DOM node with fluent builder:
div().id("card").cls("card", "elevated")
.style("background", "#fff")
.aria("label", "Product card")
.role("article")
.children(
h2().text("Product Name"),
p().text("Description here"),
a().attr("href", "/buy").text("Buy Now")
)Elements — 65+ HTML5 factory methods:
import static xss.it.jux.core.Elements.*;
// Semantic structure
header(), nav(), main_(), aside(), footer(), section(), article()
// Headings
h1(), h2(), h3(), h4(), h5(), h6()
// Text
p(), a(), strong(), em(), small(), mark(), code(), pre(), blockquote()
// Lists
ul(), ol(), li(), dl(), dt(), dd()
// Tables
table(), thead(), tbody(), tfoot(), tr(), th(), td(), caption()
// Forms
form(), input(), textarea(), select(), option(), button(), label(), fieldset()
// Media
img(src, alt), imgDecorative(src), video(), audio(), canvas(), svg(), iframe()
// Accessibility helpers
skipNav(targetId, text) // Skip-to-content link (WCAG 2.4.1)
srOnly(text) // Screen-reader-only text
liveRegion("polite") // ARIA live region for dynamic contentThere is intentionally no
img()without alt text. You cannot create an image in JUX without providing accessibility text. This is a compile-time guarantee.
PageMeta — Dynamic page metadata (returned from Page.pageMeta()):
PageMeta.create()
.title("Blog Post Title")
.titleTemplate("%s | My Site")
.description("Post excerpt for SEO")
// OpenGraph
.ogTitle("Title").ogImage("/img/cover.jpg").ogImageAlt("Cover photo")
// Twitter Cards
.twitterCard("summary_large_image").twitterSite("@myhandle")
// Resources
.css("vendor/prism.css", 50)
.js("vendor/prism.js", 50)
.removeCss("layout/sidebar.css") // Remove layout CSS on this page
// HTTP
.status(404)
.redirectTo("/new-url", 301)
.header("X-Custom", "value")
.cacheControl("public, max-age=3600")
// HTML root
.htmlLang("en").htmlDir("ltr")
.htmlAttr("data-theme", "dark")
.bodyClass("page-blog", "has-sidebar")
// Structured data
.jsonLd(articleSchemaJson)
// Performance hints
.preload("/fonts/Inter.woff2", "font")
.preconnect("https://cdn.example.com")
// Conditional
.when(post.hasCode(), meta -> meta.css("prism.css").js("prism.js"))Scans element trees for accessibility violations at render time. Not an afterthought — a first-class framework feature.
Rules enforced:
| WCAG | Rule | Severity |
|---|---|---|
| 1.1.1 | Images must have alt text | ERROR |
| 1.2.2 | Videos must have caption tracks | WARNING |
| 1.3.1 | Tables need captions, inputs need labels | ERROR |
| 1.4.4 | Font sizes must use rem/em (not px) | WARNING |
| 2.1.1 | Clickable elements must be keyboard accessible | WARNING |
| 2.4.1 | Skip navigation must be present | WARNING |
| 2.4.4 | No "click here" link text | WARNING |
| 3.1.1 |
<html lang> must be set |
ERROR |
| 4.1.1 | No duplicate IDs | ERROR |
| 4.1.2 | Proper heading hierarchy (h1 → h2 → h3) | WARNING |
JuxAccessibilityEngine engine = new JuxAccessibilityEngine();
List<A11yViolation> violations = engine.audit(elementTree);
// Each violation:
// severity: ERROR | WARNING | INFO
// wcagCriterion: "1.1.1"
// rule: "img-alt"
// message: "Image missing alt text"
// elementPath: "main > section:2 > img:1"
// suggestion: "Add alt attribute with descriptive text"Auto-fix for safe issues:
Element fixed = engine.autoFix(elementTree);
// Adds role="presentation" to decorative images
// Converts px font sizes to rem
// Adds aria-hidden to icon-only elementsTranslations are Java interfaces. Not .properties files. Not YAML. Java. Compile-time type safety — a missing translation is a compile error, not a runtime surprise.
@MessageBundle
public interface HomeMessages {
@Message("Welcome to our site")
String welcome();
@Message("Hello, {0}")
String greeting(String name);
@Message("{0,choice,0#No items|1#1 item|1<{0} items}")
String itemCount(int count);
}
@MessageBundle @MessageLocale("es")
public interface HomeMessagesEs extends HomeMessages {
@Override @Message("Bienvenido a nuestro sitio")
String welcome();
@Override @Message("Hola, {0}")
String greeting(String name);
@Override @Message("{0,choice,0#Sin artículos|1#1 artículo|1<{0} artículos}")
String itemCount(int count);
}Usage in pages — messages() is inherited from Page, no injection needed:
@Route("/")
@Localized
public class HomePage extends Page {
@Override
public Element render() {
var m = messages(); // Built into Page
return div().children(
h1().text(m.getString("hero.title")),
p().text(m.getString("greeting", "World")),
p().text(m.getString("item.count", 5)) // "5 items" / "5 artículos"
);
}
}Built-in locale-aware formatting:
var m = messages();
m.formatDate(LocalDate.now()) // "Feb 7, 2026" / "7 feb 2026"
m.formatCurrency(29.99, "USD") // "$29.99" / "29,99 $"
m.formatNumber(1250.5, 2) // "1,250.50" / "1.250,50"
m.isRtl() // true for Arabic, Hebrew, Farsi
m.currentLocale() // Locale("es")
m.availableLocales() // [en, es, fr, ...]Locale resolution chain: URL prefix → ?lang= param → cookie → session → Accept-Language header → default.
Drop jux-server on your classpath. Everything wires itself.
What gets auto-configured:
-
JuxRenderer— SSR engine (Element trees → HTML5) -
JuxRouteRegistrar— classpath scanning for@Routepages -
JuxRouteHandlerMapping— Spring MVC integration -
JuxRouteHandler— full request lifecycle orchestration -
ParameterInjector—@PathParam,@QueryParam,@CookieParam, etc. -
MetadataResolver— annotation + programmatic metadata merging -
ResourceCollector— CSS/JS deduplication, ordering, injection -
SsrCache— Caffeine-backed HTML caching -
JuxThemeResolver— cookie-based theme resolution -
JuxThemeController—POST /api/themefor no-reload theme switching -
JuxLocaleResolver— i18n locale resolution chain -
Messages— typed translation bundle service -
RouteSecurityInterceptor—@Route(roles)enforcement -
JuxAccessibilityEngine— WCAG audit on render
Route examples:
// Static page with caching
@Route(value = "/about", cacheTtl = 3600)
@Title("About Us")
// Typed path parameters
@Route("/users/{id:long}")
@PathParam private long id;
// Search with pagination
@Route("/search")
@QueryParam(required = true) private String q;
@QueryParam(defaultValue = "1") private int page;
// Form handling (programmatic via Page methods)
@Route(value = "/contact", methods = { HttpMethod.GET, HttpMethod.POST })
// In render(): if (isPost()) { var name = formParam("name"); ... }
// Secured route
@Route(value = "/admin", roles = { "ROLE_ADMIN" })
// Profile-restricted (dev-only pages)
@Route(value = "/debug", profiles = { "dev" })
// Internationalized
@Route("/about") @Localized // → /about, /es/about, /fr/about
// 404 catch-all
@Route(value = "/**", priority = Integer.MAX_VALUE)Theme switching without page reload:
// Server resolves theme from cookie → sets data-theme on <html>
// Client switches instantly:
fetch('/api/theme?value=dark', { method: 'POST' })
.then(() => document.documentElement.setAttribute('data-theme', 'dark'));CSS targets themes via attribute selectors:
:root { --bg: #ffffff; --text: #000000; }
[data-theme="dark"] { --bg: #0a0a0a; --text: #f5f5f5; }Pre-built components that are WCAG 2.2 AA compliant out of the box. Every one handles ARIA roles, keyboard navigation, and focus management correctly.
import xss.it.jux.theme.*;
// Tabs — role="tablist", arrow key navigation
child(new JuxTabs(List.of(
new JuxTabs.Tab("Overview", overviewContent),
new JuxTabs.Tab("Details", detailsContent)
)))
// Accordion — aria-expanded, Enter/Space toggle
child(new JuxAccordion(List.of(
new JuxAccordion.Section("FAQ 1", answer1),
new JuxAccordion.Section("FAQ 2", answer2)
)))
// Modal — role="dialog", focus trap, Escape to close
child(new JuxModal("Confirm Delete", confirmContent))
// Toast — role="alert", aria-live="assertive"
child(new JuxToast("Saved successfully", JuxToast.Type.SUCCESS))
// Dropdown — aria-haspopup, arrow keys, role="menu"
child(new JuxDropdown("Actions", menuItems))
// Breadcrumb — nav aria-label, aria-current="page"
child(new JuxBreadcrumb(List.of("Home", "Blog", "Post Title")))
// Pagination — nav, aria-current, aria-disabled
child(new JuxPagination(currentPage, totalPages, "/blog?page="))
// Text field — label, aria-required, aria-invalid, aria-describedby
child(new JuxTextField("email", "Email Address"))
// Progress bar — role="progressbar", aria-valuenow/min/max
child(new JuxProgress(75, "Upload progress"))
// Skip links — visible on focus, WCAG 2.4.1
child(new JuxSkipLinks())Design tokens — CSS variables generated from Java:
DesignTokens.generateCssVariables(themeProperties)
// Outputs both light and dark mode:
// :root { --jux-color-primary: #3b82f6; ... }
// [data-theme="dark"] { --jux-color-primary: #60a5fa; ... }Build pages at runtime from the database. No recompilation needed.
Concepts:
-
PageDefinition— a page stored in the database with slug, layout, per-locale content -
WidgetInstance— a widget placed on a page with type, props, styling, and a11y config -
WidgetRegistry— central catalog of available widget types -
PropSchema— describes what props each widget accepts (for admin panel form generation)
Register custom widgets:
@Configuration
public class MyWidgets {
@Bean
public WidgetRegistration heroWidget(WidgetRegistry registry) {
return registry.register("hero",
HeroWidget.class,
"Hero Banner",
"image",
WidgetCategory.HERO,
PropSchema.builder()
.prop("heading", PropType.STRING, "Main heading", true)
.prop("ctaText", PropType.STRING, "Button label", false)
.prop("ctaUrl", PropType.URL, "Button link", false)
.build()
);
}
}Widget styling (database-stored):
{
"id": "hero-1",
"type": "hero",
"props": { "heading": "Welcome", "ctaText": "Get Started", "ctaUrl": "/signup" },
"style": {
"background": { "type": "GRADIENT", "gradientCss": "linear-gradient(135deg, #667eea, #764ba2)" },
"padding": "6rem 2rem",
"minHeight": "80vh"
},
"a11y": { "ariaLabel": "Hero banner", "role": "banner" }
}A reactive data layer for JUX. Not required — JUX works without it. Add it when you need observable state, data binding, or computed values.
implementation 'io.github.xdsswar:jux-reactive:1.0.0'<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-reactive</artifactId>
<version>1.0.0</version>
</dependency>Properties — type-safe, change-tracking wrappers:
var name = new SimpleStringProperty("Alice");
var age = new SimpleIntegerProperty(30);
// Listen for changes
name.addListener((obs, oldVal, newVal) ->
System.out.println("Name changed: " + oldVal + " → " + newVal));
name.set("Bob"); // triggers listenerBindings — computed values that auto-update:
var firstName = new SimpleStringProperty("Jane");
var lastName = new SimpleStringProperty("Doe");
// Computed binding — updates automatically when either property changes
StringBinding fullName = Bindings.concat(firstName, " ", lastName);
fullName.get(); // "Jane Doe"
firstName.set("John");
fullName.get(); // "John Doe"Observable collections — lists, sets, maps with change tracking:
ObservableList<String> items = JuxCollections.observableArrayList();
items.addListener((ListChangeListener<String>) change -> {
while (change.next()) {
if (change.wasAdded()) System.out.println("Added: " + change.getAddedSubList());
if (change.wasRemoved()) System.out.println("Removed: " + change.getRemoved());
}
});
items.addAll("Alpha", "Beta", "Gamma");
// Filtered and sorted views — live, no copying
FilteredList<String> filtered = new FilteredList<>(items, s -> s.startsWith("A"));
SortedList<String> sorted = new SortedList<>(items, Comparator.reverseOrder());What's included:
-
SimpleObjectProperty<T>,SimpleStringProperty,SimpleIntegerProperty,SimpleLongProperty,SimpleDoubleProperty,SimpleBooleanProperty -
ReadOnlyObjectProperty<T>and typed read-only variants -
ObjectBinding<T>,StringBinding,IntegerBinding,LongBinding,DoubleBinding,BooleanBinding -
Bindingsutility —add,subtract,multiply,divide,concat,equal,greaterThan,when/then/otherwise, and more -
ObservableList<E>,ObservableSet<E>,ObservableMap<K,V>withFilteredList,SortedList - Bidirectional binding, lazy evaluation, invalidation listeners
Timeline-based animation engine for JUX. Not required — add it when you need property animations, transitions, or sequenced effects.
Depends on jux-reactive (animates reactive properties).
implementation 'io.github.xdsswar:jux-animations:1.0.0'<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-animations</artifactId>
<version>1.0.0</version>
</dependency>Timeline — keyframe-based animation:
var opacity = new SimpleDoubleProperty(0.0);
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue<>(opacity, 0.0)),
new KeyFrame(Duration.seconds(1),
new KeyValue<>(opacity, 1.0, Interpolator.EASE_IN_OUT))
);
timeline.play(); // opacity animates 0 → 1 over 1 secondBuilt-in transitions:
// Fade in/out
new FadeTransition(Duration.millis(500), opacityProp)
.setFromValue(0.0).setToValue(1.0).play();
// Scale
new ScaleTransition(Duration.seconds(0.3), scaleProp)
.setFromValue(0.8).setToValue(1.0).play();
// Slide
new SlideTransition(Duration.millis(400), xProp)
.setFromValue(-100).setToValue(0).play();
// Rotate
new RotateTransition(Duration.seconds(1), angleProp)
.setFromValue(0).setToValue(360).play();
// Compose — parallel or sequential
new SequentialTransition(fadeIn, pause, slideOut).play();
new ParallelTransition(fadeIn, scaleUp).play();Features:
-
Duration— immutable time values (ZERO,INDEFINITE,millis(),seconds(),minutes()) -
Interpolator— built-in easings:LINEAR,EASE_IN,EASE_OUT,EASE_IN_OUT,DISCRETE - Cycle control — repeat count, auto-reverse, rate scaling
- Status tracking —
STOPPED,RUNNING,PAUSEDwith observable status property -
FrameScheduler— pluggable frame callback (maps torequestAnimationFrameon web) -
TestFrameScheduler— deterministic time control for unit tests
Load external HTML files into JUX components. Not required — JUX works purely with the Java Element API. Add it when you prefer writing markup in HTML files with Java-powered wiring.
implementation 'io.github.xdsswar:jux-html:1.0.0'<dependency>
<groupId>io.github.xdsswar</groupId>
<artifactId>jux-html</artifactId>
<version>1.0.0</version>
</dependency>Define a template:
<!-- src/main/resources/templates/pages/profile.html -->
<section class="profile">
<h2 id="title">#{profile.title}</h2>
<div id="avatar"></div>
<div data-slot="content"></div>
</section>Wire it to a component:
@Html("pages/profile.html")
public class ProfileCard extends Component {
@HtmlId private Element title; // injected by id="title"
@HtmlId private Element avatar; // injected by id="avatar"
@Slot("content") private Element content; // injected by data-slot="content"
@Override
public Element render() {
return HtmlLoader.load(this); // parses, caches, injects, returns wired template
}
}Features:
-
@Html(path)— associates a component with an HTML template file -
@HtmlId— injects elements by their HTMLidattribute -
@Slot(name)— injects named slots (data-slot="...") for dynamic content -
HtmlParser— full HTML5 parser (handles void elements, character entities, boolean attributes) -
TemplateCache— LRU cache of parsed templates (parse once, clone per use) -
ExpressionResolver— evaluates#{...}placeholders for i18n keys -
I18nResolver,FormatResolver,FormatDirective— locale-aware formatting in templates - Error reporting with filename + line number
Annotation processor that catches errors at compile time:
-
@Routepath syntax validation -
@Messagepattern syntax validation - Missing
@MessageLocaleimplementations - Duplicate route paths
Compiles Java to JavaScript via TeaVM 0.13.x. Client-side components get:
- DOM hydration from server-rendered HTML
- Reactive
@Statefields — changes trigger re-render + DOM diff/patch - Event binding via
@Onannotations - Direct DOM access via
org.teavm.jso.dom.html.*
@JuxComponent(clientSide = true)
public class Counter extends Component {
@State private int count = 0;
@Override
public Element render() {
return div().children(
span().text("Count: " + count),
button().text("+").on("click", e -> count++)
);
}
}package com.example.pages;
import xss.it.jux.annotation.*;
import xss.it.jux.core.*;
import com.example.service.BlogRepository;
import static xss.it.jux.core.Elements.*;
@Route("/blog/{slug}")
@Localized
@Css(value = "themes/default.css", order = 1)
@Css(value = "pages/blog.css", order = 10)
public class BlogPostPage extends Page {
@PathParam private String slug;
private final BlogRepository blogRepo;
public BlogPostPage(BlogRepository blogRepo) {
this.blogRepo = blogRepo;
}
@Override
public PageMeta pageMeta() {
var post = blogRepo.findBySlug(slug).orElse(null);
if (post == null) {
return PageMeta.create().status(404).title("Not Found");
}
return PageMeta.create()
.title(post.getTitle())
.titleTemplate("%s | My Blog")
.description(post.getExcerpt())
.ogTitle(post.getTitle())
.ogImage(post.getCoverImage())
.ogImageAlt(post.getCoverImageAlt())
.twitterCard("summary_large_image")
.htmlLang(locale().getLanguage())
.autoDir()
.canonical("https://example.com/blog/" + slug)
.when(post.hasCodeBlocks(), meta ->
meta.css("vendor/prism.css", 50).js("vendor/prism.js", 50)
);
}
@Override
public Element render() {
var post = blogRepo.findBySlug(slug).orElse(null);
if (post == null) {
return main_().children(
h1().text("404"),
p().text("Post not found."),
a().attr("href", "/blog").text("Back to blog")
);
}
return article().cls("blog-post").children(
header().children(
h1().text(post.getTitle()),
time().attr("datetime", post.getDate().toString())
.text(messages().formatDate(post.getDate())),
span().cls("author").text(post.getAuthor())
),
div().cls("content").text(post.getContent()),
footer().children(
nav().aria("label", "Post navigation").children(
post.getPrevious() != null
? a().attr("href", "/blog/" + post.getPrevious()).text("Previous")
: null,
post.getNext() != null
? a().attr("href", "/blog/" + post.getNext()).text("Next")
: null
)
)
);
}
}@Route(value = "/contact", methods = { HttpMethod.GET, HttpMethod.POST })
@Title("Contact Us")
public class ContactPage extends Page {
private final ContactService contacts;
public ContactPage(ContactService contacts) {
this.contacts = contacts;
}
@Override
public Element render() {
if (isPost()) {
contacts.submit(formParam("name"), formParam("email"), formParam("message"));
return div().cls("success").children(h1().text("Thank you!"));
}
return form().attr("method", "post").children(
label().attr("for", "name").text("Name"),
input().id("name").attr("name", "name").attr("type", "text").ariaRequired(true),
label().attr("for", "email").text("Email"),
input().id("email").attr("name", "email").attr("type", "email").ariaRequired(true),
label().attr("for", "msg").text("Message"),
textarea().id("msg").attr("name", "message").ariaRequired(true),
button().attr("type", "submit").text("Send")
);
}
}public class LanguageSwitcher extends Component {
private final JuxMessages messages;
public LanguageSwitcher(JuxMessages messages) {
this.messages = messages;
}
@Override
public Element render() {
var current = messages.currentLocale();
return nav().aria("label", "Language selection").children(
ul().cls("lang-switcher").children(
messages.availableLocales().stream().map(locale ->
li().children(
a().attr("href", "/" + locale.getLanguage() + "/")
.attr("hreflang", locale.getLanguage())
.lang(locale.getLanguage())
.ariaCurrent(locale.equals(current) ? "true" : null)
.text(locale.getDisplayLanguage(locale))
)
).toList()
)
);
}
}Pages are Spring beans. Services are injected via constructor injection — Spring Boot 3.x auto-wires single-constructor beans without @Autowired:
@Route("/dashboard")
@Title("Dashboard")
public class DashboardPage extends Page {
private final UserService users;
private final AnalyticsService analytics;
// Spring auto-injects both — no @Autowired needed
public DashboardPage(UserService users, AnalyticsService analytics) {
this.users = users;
this.analytics = analytics;
}
@Override
public Element render() {
var m = messages(); // Built into Page, no injection
var user = users.current();
var stats = analytics.dashboardStats();
return main_().children(
h1().text(m.getString("dashboard.welcome", user.getName())),
new StatsGrid(stats).render(), // Component — instantiated with new
new RecentActivity(user.getId()).render()
);
}
}The rule is simple:
-
Spring services (repositories, services, configs) → constructor injection on
Page -
Request data (params, locale, messages, cookies) → inherited methods from
Page -
Components (UI building blocks) →
newwith constructor arguments
JUX ships with two demo applications that showcase the framework in action. Both are fully working Spring Boot apps — run them and explore.
A feature-rich demo that exercises every JUX capability: pages, layouts, components, themes, forms, i18n, and blog with dynamic routes.
./gradlew :jux-demo:bootRun
# → http://localhost:9090| Page | Route | Demonstrates |
|---|---|---|
| Home | / |
Hero, feature cards, testimonials, newsletter, CTA |
| Components | /components |
Tabs, accordion, breadcrumb, pagination, toast, text fields |
| Blog | /blog |
Card grid, tag filtering |
| Blog Post | /blog/{slug} |
@PathParam, dynamic pageMeta(), breadcrumbs, 404 handling |
| About | /about |
Team cards, stats, values |
| Contact | /contact |
Form GET+POST handling via @RequestContext
|
| Dashboard | /dashboard |
Stats grid, tables, activity feed |
| Pricing | /pricing |
Pricing cards with feature lists |
| Login | /login |
Form layout, field validation |
| Gallery | /gallery |
Image grid |
| Search | /search?q= |
Query params, empty/results states |
| 404 | /** |
Catch-all with priority = MAX_VALUE
|
Stack: Bootstrap 5 CSS · English + Spanish i18n · Light/dark theme switching
A complete e-commerce storefront proving JUX is CSS-framework-agnostic. Uses Tailwind CSS instead of Bootstrap, and showcases JSON API controllers coexisting with JUX @Route pages in the same project.
./gradlew :jux-store:bootRun
# → http://localhost:9091Pages (11 routes):
| Page | Route | Highlights |
|---|---|---|
| Home | / |
Hero banner, featured products, categories, promo banner, newsletter |
| Product Catalog | /products |
Filter pills, sort dropdown, JuxPagination, query params |
| Product Detail | /products/{slug} |
Breadcrumb, JuxTabs (description/specs/reviews), related products |
| Categories | /categories |
Category cards grid with product counts |
| Category | /categories/{slug} |
@PathParam, filtered product grid, 404 handling |
| Cart | /cart |
Cart items, quantity display, order summary sidebar |
| Checkout | /checkout |
Multi-section form (GET+POST), shipping + payment fieldsets |
| Search | /search?q= |
Search bar, results grid, empty/no-results states |
| About | /about |
Values cards, stats counters, team section |
| Contact | /contact |
Contact form (GET+POST), info sidebar with icons |
| 404 | /** |
Styled 404 with navigation links |
JSON API endpoints (Spring @RestController alongside JUX pages):
GET /api/products # List all, ?category=electronics&sort=price-asc
GET /api/products/{slug} # Single product detail
GET /api/cart # Sample cart with totals
POST /api/cart/add # {"slug": "...", "quantity": 1}
POST /api/cart/remove # {"slug": "..."}
GET /api/search?q=wireless # Product search
Data: 20 hardcoded products across 5 categories (Electronics, Clothing, Home & Kitchen, Sports & Outdoors, Books), reviews, and a sample cart — no database required.
Stack: Tailwind CSS 2.2 · English + Spanish i18n · 13 reusable components · 3 REST API controllers
Key patterns demonstrated:
- CSS-agnostic — same JUX framework, different CSS library (Tailwind vs Bootstrap)
-
REST + SSR coexistence —
@RestControllerJSON APIs alongside@RouteHTML pages -
Reusable components —
ProductCard,ProductGrid,RatingStars,PriceDisplay,EmptyState, etc. -
ADA compliance — every
<img>has alt text, every<input>has a<label>,ariaRequiredon required fields, skip-nav, sr-only text, semantic HTML throughout
A comprehensive demo showcasing JUX's client-side capabilities — all interactivity compiled from Java to JavaScript via TeaVM. No hand-written JS anywhere.
./gradlew :client-side-demo:bootRun
# → http://localhost:9092Pages:
| Page | Route | Demonstrates |
|---|---|---|
| Home | / |
Overview of all interactive demos |
| Components | /components |
Pure client-side widgets (no API) |
| API Demo | /api-demo |
Client-side widgets calling REST APIs |
| HTML Demo | /html-demo |
@Html template loading with @HtmlId and @Slot
|
| About | /about |
Framework info |
Interactive components (Java → TeaVM → JS):
| Widget | What it does |
|---|---|
CounterWidget |
Increment/decrement/reset with reactive @State
|
TodoWidget |
Add/complete/delete todos, filtered views |
LiveSearchWidget |
Debounced search with instant filtering |
StopwatchWidget |
Start/stop/lap timer using Window.setInterval
|
TabsWidget |
Tab switching with keyboard navigation |
AccordionWidget |
Expand/collapse sections |
ModalWidget |
Dialog with focus trap, Escape to close |
ThemeToggleWidget |
Light/dark theme switching |
API-connected components (real HTTP via TeaVM XMLHttpRequest):
| Widget | API Endpoint | What it does |
|---|---|---|
UserBrowserWidget |
GET /api/users |
Fetches and displays user cards with filtering |
QuoteMachineWidget |
GET /api/quotes/random |
Fetches random quotes on demand |
FormSubmitWidget |
POST /api/contact |
Form submission with server-side validation errors |
PollWidget |
GET /api/stats |
Live polling every 5s with auto-refresh |
HTML template components (@Html + @HtmlId + @Slot):
| Component | Template | What it does |
|---|---|---|
ProfileCardHtml |
profile-card.html |
Profile card loaded from HTML with slot injection |
DashboardPanelHtml |
dashboard-panel.html |
Dashboard panel with multiple @HtmlId elements |
PricingTableHtml |
pricing-table.html |
Pricing table parsed from HTML template |
TimelineHtml |
timeline.html |
Timeline component with #{...} i18n placeholders |
Stack: Tailwind CSS · TeaVM 0.13.x · 4 REST API controllers · 21 components
Key patterns demonstrated:
- Zero hand-written JS — all DOM manipulation, event handling, HTTP requests, and timers compiled from Java via TeaVM
-
Native TeaVM APIs —
XMLHttpRequest,JSON.parse(),@JSPropertyoverlay interfaces,Window.setInterval/clearInterval -
Handler registry — one permanent
addEventListenerper element, handler references swapped on re-render -
Reactive properties —
jux-reactiveproperties driving client-side UI updates -
HTML template loading —
jux-htmlmodule parsing external HTML files into JUXElementtrees
Full application.yml reference:
jux:
# Package scanning — where your @Route pages live
base-package: com.example.pages
# Server-side rendering
ssr:
cache:
enabled: true
max-size: 1000
ttl: 300s
# Theme system
theme:
name: default
default-theme: light
available-themes:
- light
- dark
cookie-name: jux-theme
cookie-max-age: 31536000 # 1 year
# Accessibility (WCAG 2.2 AA)
a11y:
enabled: true
audit-on-render: true # Run audit on every render (dev mode)
fail-on-error: false # 500 on ERROR violations
log-violations: true
auto-fix: true
# Internationalization
i18n:
enabled: true
default-locale: en
locales: [ en, es, fr, de, ar, pt-BR, zh-CN ]
fallback-strategy: default # default | chain | none
url-strategy: prefix # prefix | subdomain | parameter
cookie-name: jux-lang
cookie-max-age: 31536000
generate-hreflang: true
# CMS (optional)
cms:
enabled: true
home-slug: home
error-slug: "404"
admin:
enabled: true
path: /admin
roles: [ ROLE_ADMIN, ROLE_EDITOR ]| Module | Package | Required | Depends On |
|---|---|---|---|
jux-annotations |
xss.it.jux.annotation |
yes | nothing |
jux-core |
xss.it.jux.core |
yes | jux-annotations |
jux-a11y |
xss.it.jux.a11y |
yes | jux-core |
jux-i18n |
xss.it.jux.i18n |
yes | jux-core |
jux-processor |
xss.it.jux.processor |
yes | jux-annotations, jux-core |
jux-client |
xss.it.jux.client |
yes | jux-core, jux-annotations, jux-a11y |
jux-server |
xss.it.jux.server |
yes | jux-core, jux-a11y, jux-i18n, jux-processor |
jux-themes |
xss.it.jux.theme |
no | jux-core, jux-a11y |
jux-cms |
xss.it.jux.cms |
no | jux-server, jux-a11y, jux-i18n |
jux-reactive |
xss.it.jux.reactive |
no | jux-core |
jux-animations |
xss.it.jux.animations |
no | jux-reactive |
jux-html |
xss.it.jux.html |
no | jux-core, jux-i18n, jux-reactive |
Optional modules (
jux-reactive,jux-animations,jux-html,jux-themes,jux-cms) are not required for JUX to work. The framework is fully functional with just the core modules. Add optional modules only when you need their capabilities.
| Component | Version | Purpose |
|---|---|---|
| Java | 25 | Language runtime |
| Spring Boot | 3.5.10 | Web MVC, DI, JPA, Security, Auto-config |
| TeaVM | 0.13.x | Java → JavaScript compilation |
| Caffeine | (managed) | SSR page caching |
| Lombok | 1.18.42 | Boilerplate reduction |
| JUnit 5 | 5.11.4 | Testing |
| AssertJ | 3.27.3 | Fluent test assertions |
| Gradle | 9.1+ | Build system (Groovy DSL) |
No npm. No webpack. No vite. No node_modules. Just Gradle.
# Build all modules
./gradlew build
# Run tests
./gradlew test
# Publish to local Maven (for consumer projects)
./gradlew publishToMavenLocal
# Compile client-side JS (TeaVM)
./gradlew :jux-client:generateJavaScript
# Run individual module tests
./gradlew :jux-core:test
./gradlew :jux-a11y:test
./gradlew :jux-server:test
./gradlew :jux-themes:test
./gradlew :jux-reactive:test
./gradlew :jux-animations:test
./gradlew :jux-html:test
# Run demo apps
./gradlew :jux-demo:bootRun # → http://localhost:9090
./gradlew :jux-store:bootRun # → http://localhost:9091
./gradlew :client-side-demo:bootRun # → http://localhost:9092Requirements: Java 25+, Gradle 9.1+
This is a concept framework.
JUX is a working proof of concept. It compiles, it renders, it serves pages, it passes 985 tests. But it's not battle-tested in production. APIs will change. Modules will evolve. Things will break and get rebuilt better.
The core idea — build entire websites in pure Java with no template languages, no JavaScript authoring, and ADA compliance by default — is the part that won't change.
Everything else is negotiable.
Xtreme Software Solutions Source License v1.0
- Open-source projects: Free to use, modify, and distribute.
- Commercial / paid projects: Requires explicit written permission from Xtreme Software Solutions.
- No warranty: Provided "AS IS". Use at your own risk. The author is not responsible for any issues arising from the use of this software.
See LICENSE for full terms.
Copyright (c) 2026 Xtreme Software Solutions (XDSSWAR). All rights reserved.






