Fragments
Server-driven UI without writing JavaScript: any route can return a fragment (an Element without html/body), and declarative swap attributes fetch it and insert it into the page. Swaps are wrapped in a View Transition when the browser supports it.
Basic Swap
The server route returns a fragment; any element triggers the swap on click.
QueryParam<Integer> PAGE = QueryParam.of("page", Integer.class).orElse(1);
Element productList(int page) { return div("Page " + page); }
// Server: returns just the list markup
app.get("/products/list", req ->
productList(PAGE.from(req)));
// Client: zero JavaScript written
button(swap("/products/list?page=2", "#products"),
"Next page")Progressive Forms
swapForm posts the form data and swaps the returned fragment into the target. With the action attribute set, the form still works when JavaScript is disabled — it just submits natively to the same route.
form(action("/contact/submit"), method("post"), // no-JS fallback
swapForm("/contact/submit", "#form-status"), // progressive swap
label(for_("name"), "Name"),
input(type("text"), id("name"), name("name"), placeholder("Your name"), required()),
div(id("form-status")),
button(type("submit"), "Send"))
// The route returns a status fragment:
app.post("/contact/submit", ctx -> {
messageStore.save(ctx.formParam("name"), ...);
return ContactStatus.success("Message sent!");
});Morphing
swapMorph updates the target in place instead of replacing its HTML: unchanged nodes are kept, so focus, scroll position, and in-progress input survive. Ideal for lists or dashboards that refresh while the user is interacting.
div(id("inbox"), swapMorph("/inbox/refresh", "#inbox"))History & Events
- swapPush(url) adds a browser-history entry; back/forward re-swap naturally
- swapOuter(url, sel) replaces the target element itself (outerHTML)
- a jweb:swap CustomEvent fires on document after every swap