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.

// Server: returns just the list markup
app.get("/products/list", req ->
    productList(PAGE.from(req)));

// Client: zero JavaScript written
button(attrs().swap("/products/list?page=2", "#products"),
    text("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(attrs().action("/contact/submit").method("post")   // no-JS fallback
        .swapForm("/contact/submit", "#form-status"),   // progressive swap
    field("Name", "name", "text", "Your name"),
    div(attrs().id("form-status")),
    submitButton("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(attrs().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
Fragment responses are served clean — the framework skips script injection for Elements without a <body>, so swapped content never duplicates the runtime.